Skip to content

Commit 9cecfa2

Browse files
committed
feat: hasura mapping support
1 parent 9295bc6 commit 9cecfa2

12 files changed

+360
-49
lines changed

lib/utils/token/getDecodedToken.ts

+41-9
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,51 @@
1-
import { jwtDecoder, JWTDecoded } from "@kinde/jwt-decoder";
1+
import { jwtDecoder, JWTDecoded as JWTBase } from "@kinde/jwt-decoder";
22
import { getActiveStorage } from ".";
33
import { StorageKeys } from "../../sessionManager";
44
/**
55
*
66
* @param tokenType Type of token to decode
77
* @returns { Promise<JWTDecoded | null> }
88
*/
9-
export const getDecodedToken = async <
10-
T = JWTDecoded & {
11-
permissions: string[];
12-
org_code: string;
13-
},
14-
>(
9+
10+
type JWTExtra = {
11+
"x-hasura-permissions": never;
12+
"x-hasura-org-code": never;
13+
"x-hasura-org-codes": never;
14+
"x-hasura-roles": never;
15+
"x-hasura-feature-flags": never;
16+
17+
feature_flags: Record<
18+
string,
19+
{ t: "b" | "i" | "s"; v: string | boolean | number | object }
20+
>;
21+
permissions: string[];
22+
org_code: string;
23+
org_codes: string[];
24+
roles: string[];
25+
};
26+
27+
type JWTExtraHasura = {
28+
"x-hasura-permissions": string[];
29+
"x-hasura-org-code": string;
30+
"x-hasura-org-codes": string[];
31+
"x-hasura-roles": string[];
32+
"x-hasura-feature-flags": Record<
33+
string,
34+
{ t: "b" | "i" | "s"; v: string | boolean | number | object }
35+
>;
36+
37+
feature_flags: never;
38+
permissions: never;
39+
org_codes: never;
40+
org_code: never;
41+
roles: never;
42+
};
43+
44+
type JWTDecoded = JWTBase & (JWTExtra | JWTExtraHasura);
45+
46+
export const getDecodedToken = async <T = JWTDecoded>(
1547
tokenType: "accessToken" | "idToken" = StorageKeys.accessToken,
16-
): Promise<T | null> => {
48+
): Promise<(T & JWTDecoded) | null> => {
1749
const activeStorage = getActiveStorage();
1850

1951
if (!activeStorage) {
@@ -28,7 +60,7 @@ export const getDecodedToken = async <
2860
return null;
2961
}
3062

31-
const decodedToken = jwtDecoder<T>(token);
63+
const decodedToken = jwtDecoder<T & JWTDecoded>(token);
3264

3365
if (!decodedToken) {
3466
console.warn("No decoded token found");

lib/utils/token/getFlag.ts

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
1-
import { getClaim } from "./getClaim";
1+
import { getDecodedToken } from ".";
22

33
/**
44
*
55
* @param keyName key to get from the token
66
* @returns { Promise<string | number | string[] | null> }
77
*/
8-
export const getFlag = async <T = string | boolean | number>(
8+
export const getFlag = async <T = string | boolean | number | object>(
99
name: string,
1010
): Promise<T | null> => {
11-
const flags = (
12-
await getClaim<
13-
{ feature_flags: string },
14-
Record<string, { t: "b" | "i" | "s"; v: T }>
15-
>("feature_flags")
16-
)?.value;
11+
const claims = await getDecodedToken();
12+
13+
if (!claims) {
14+
return null;
15+
}
16+
17+
const flags = claims.feature_flags || claims["x-hasura-feature-flags"];
18+
if (!flags) {
19+
return null;
20+
}
1721

1822
if (name && flags) {
1923
const value = flags[name];
20-
return value ? value?.v : null;
24+
return value ? (value?.v as T) : null;
2125
}
2226
return null;
2327
};

lib/utils/token/getFlagHasura.test.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { describe, expect, it, beforeEach } from "vitest";
2+
import { MemoryStorage, StorageKeys } from "../../sessionManager";
3+
import { setActiveStorage, getFlag } from ".";
4+
import { createMockAccessToken } from "./testUtils";
5+
6+
const storage = new MemoryStorage();
7+
8+
describe("getFlag - Hasura", () => {
9+
beforeEach(() => {
10+
setActiveStorage(storage);
11+
});
12+
13+
it("when no token", async () => {
14+
await storage.setSessionItem(StorageKeys.idToken, null);
15+
const idToken = await getFlag("test");
16+
17+
expect(idToken).toStrictEqual(null);
18+
});
19+
20+
it("boolean true", async () => {
21+
await storage.setSessionItem(
22+
StorageKeys.accessToken,
23+
createMockAccessToken({
24+
["x-hasura-feature-flags"]: {
25+
test: {
26+
v: true,
27+
t: "b",
28+
},
29+
},
30+
}),
31+
);
32+
const idToken = await getFlag<boolean>("test");
33+
34+
expect(idToken).toStrictEqual(true);
35+
});
36+
37+
it("boolean false", async () => {
38+
await storage.setSessionItem(
39+
StorageKeys.accessToken,
40+
createMockAccessToken({
41+
["x-hasura-feature-flags"]: {
42+
test: {
43+
v: false,
44+
t: "b",
45+
},
46+
},
47+
}),
48+
);
49+
const idToken = await getFlag<boolean>("test");
50+
51+
expect(idToken).toStrictEqual(false);
52+
});
53+
54+
it("string", async () => {
55+
await storage.setSessionItem(
56+
StorageKeys.accessToken,
57+
createMockAccessToken({
58+
["x-hasura-feature-flags"]: {
59+
test: {
60+
v: "hello",
61+
t: "s",
62+
},
63+
},
64+
}),
65+
);
66+
const idToken = await getFlag<string>("test");
67+
68+
expect(idToken).toStrictEqual("hello");
69+
});
70+
71+
it("integer", async () => {
72+
await storage.setSessionItem(
73+
StorageKeys.accessToken,
74+
createMockAccessToken({
75+
feature_flags: {
76+
test: {
77+
v: 5,
78+
t: "i",
79+
},
80+
},
81+
}),
82+
);
83+
const idToken = await getFlag<number>("test");
84+
85+
expect(idToken).toStrictEqual(5);
86+
});
87+
88+
it("no existing flag", async () => {
89+
await storage.setSessionItem(
90+
StorageKeys.accessToken,
91+
createMockAccessToken({
92+
["x-hasura-feature-flags"]: {
93+
test: {
94+
v: 5,
95+
t: "i",
96+
},
97+
},
98+
}),
99+
);
100+
const idToken = await getFlag<number>("noexist");
101+
102+
expect(idToken).toStrictEqual(null);
103+
});
104+
});

lib/utils/token/getPermissions.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ export const getPermissions = async <T = string>(): Promise<Permissions<T>> => {
1414
permissions: [],
1515
};
1616
}
17+
const permissions = token.permissions || token["x-hasura-permissions"] || [];
18+
const orgCode = token.org_code || token["x-hasura-org-code"];
1719

18-
const permissions = token.permissions || [];
1920
return {
20-
orgCode: token.org_code,
21+
orgCode,
2122
permissions: permissions as T[],
2223
};
2324
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, it, beforeEach } from "vitest";
2+
import { MemoryStorage, StorageKeys } from "../../sessionManager";
3+
import { setActiveStorage } from ".";
4+
import { createMockAccessToken } from "./testUtils";
5+
import { getPermissions } from ".";
6+
7+
const storage = new MemoryStorage();
8+
9+
enum PermissionEnum {
10+
canEdit = "canEdit",
11+
}
12+
13+
describe("getPermissions - Hasura", () => {
14+
beforeEach(() => {
15+
setActiveStorage(storage);
16+
});
17+
18+
it("when no token", async () => {
19+
await storage.setSessionItem(StorageKeys.idToken, null);
20+
const idToken = await getPermissions();
21+
22+
expect(idToken).toStrictEqual({
23+
orgCode: null,
24+
permissions: [],
25+
});
26+
});
27+
28+
it("with value", async () => {
29+
await storage.setSessionItem(
30+
StorageKeys.accessToken,
31+
createMockAccessToken({ "x-hasura-permissions": ["canEdit"] }),
32+
);
33+
const idToken = await getPermissions();
34+
35+
expect(idToken).toStrictEqual({
36+
orgCode: "org_123456789",
37+
permissions: ["canEdit"],
38+
});
39+
});
40+
41+
it("with value and typed permissions", async () => {
42+
await storage.setSessionItem(
43+
StorageKeys.accessToken,
44+
createMockAccessToken({ permissions: ["canEdit"] }),
45+
);
46+
const idToken = await getPermissions<PermissionEnum>();
47+
48+
expect(idToken).toStrictEqual({
49+
orgCode: "org_123456789",
50+
permissions: [PermissionEnum.canEdit],
51+
});
52+
});
53+
54+
it("no permissions array", async () => {
55+
await storage.setSessionItem(
56+
StorageKeys.accessToken,
57+
createMockAccessToken({ permissions: null }),
58+
);
59+
const idToken = await getPermissions<PermissionEnum>();
60+
61+
expect(idToken).toStrictEqual({
62+
orgCode: "org_123456789",
63+
permissions: [],
64+
});
65+
});
66+
});

lib/utils/token/getRoles.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { JWTDecoded } from "@kinde/jwt-decoder";
21
import { getDecodedToken } from ".";
32

43
export type Permissions<T> = { orgCode: string | null; permissions: T[] };
@@ -7,18 +6,18 @@ export type Permissions<T> = { orgCode: string | null; permissions: T[] };
76
* @returns { Promise<Permissions> }
87
*/
98
export const getRoles = async (): Promise<string[]> => {
10-
const token = await getDecodedToken<JWTDecoded & { roles: string[] }>();
9+
const token = await getDecodedToken();
1110

1211
if (!token) {
1312
return [];
1413
}
1514

16-
if (!token.roles) {
15+
if (!token.roles && !token["x-hasura-roles"]) {
1716
console.warn(
1817
"No roles found in token, ensure roles have been included in the token customisation within the application settings",
1918
);
2019
return [];
2120
}
2221

23-
return token.roles;
22+
return token.roles || token["x-hasura-roles"] || [];
2423
};
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it, beforeEach, vi } from "vitest";
2+
import { MemoryStorage, StorageKeys } from "../../sessionManager";
3+
import { setActiveStorage } from ".";
4+
import { createMockAccessToken } from "./testUtils";
5+
import { getRoles } from ".";
6+
7+
const storage = new MemoryStorage();
8+
9+
describe("getRoles - Hasura", () => {
10+
beforeEach(() => {
11+
setActiveStorage(storage);
12+
});
13+
14+
it("when no token", async () => {
15+
await storage.setSessionItem(StorageKeys.idToken, null);
16+
const idToken = await getRoles();
17+
18+
expect(idToken).toStrictEqual([]);
19+
});
20+
21+
it("with token no roles", async () => {
22+
await storage.setSessionItem(
23+
StorageKeys.accessToken,
24+
createMockAccessToken({ roles: undefined }),
25+
);
26+
const idToken = await getRoles();
27+
expect(idToken).toStrictEqual([]);
28+
});
29+
30+
it("warns when token no roles", async () => {
31+
const consoleMock = vi
32+
.spyOn(console, "warn")
33+
.mockImplementation(() => undefined);
34+
35+
await storage.setSessionItem(
36+
StorageKeys.accessToken,
37+
createMockAccessToken({ "x-hasura-roles": undefined }),
38+
);
39+
await getRoles();
40+
expect(consoleMock).toHaveBeenCalledWith(
41+
"No roles found in token, ensure roles have been included in the token customisation within the application settings",
42+
);
43+
});
44+
45+
it("with value and typed permissions", async () => {
46+
await storage.setSessionItem(
47+
StorageKeys.accessToken,
48+
createMockAccessToken({ "x-hasura-roles": ["admin"] }),
49+
);
50+
const idToken = await getRoles();
51+
52+
expect(idToken).toStrictEqual(["admin"]);
53+
});
54+
});

lib/utils/token/getUserOrganistaions.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { getDecodedToken } from ".";
55
* @returns { Promise<string[] | null> }
66
*/
77
export const getUserOrganizations = async (): Promise<string[] | null> => {
8-
return (
9-
(
10-
await getDecodedToken<{
11-
org_codes: string[];
12-
}>("idToken")
13-
)?.org_codes || null
14-
);
8+
const token = await getDecodedToken<{
9+
org_codes: string[];
10+
}>("idToken");
11+
12+
if (!token) {
13+
return null;
14+
}
15+
16+
return token.org_codes || token["x-hasura-org-codes"] || null;
1517
};

0 commit comments

Comments
 (0)