@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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
81
|
+
"gitHead": "a46ebe234551d76d6a0ea9095be6e55540006ab3"
|
|
82
82
|
}
|