Skip to content

Commit 68f27cc

Browse files
authored
Merge pull request #86 from kinde-oss/feat/hasura
2 parents 9295bc6 + 3ea032c commit 68f27cc

19 files changed

+492
-65
lines changed

lib/utils/token/getClaim.test.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, it, beforeEach } from "vitest";
2+
import { getClaim, setActiveStorage } from ".";
3+
import { createMockAccessToken } from "./testUtils";
4+
import { MemoryStorage, StorageKeys } from "../../main";
5+
6+
const storage = new MemoryStorage();
7+
8+
describe("getClaim", () => {
9+
beforeEach(() => {
10+
setActiveStorage(storage);
11+
});
12+
13+
it("when no token", async () => {
14+
await storage.setSessionItem(StorageKeys.accessToken, null);
15+
const value = await getClaim("test");
16+
expect(value).toStrictEqual(null);
17+
});
18+
19+
it("get claim string value", async () => {
20+
await storage.setSessionItem(
21+
StorageKeys.accessToken,
22+
createMockAccessToken({ test: "org_123456" }),
23+
);
24+
const value = await getClaim("test");
25+
expect(value).toStrictEqual({
26+
name: "test",
27+
value: "org_123456",
28+
});
29+
});
30+
});
+8-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import { getClaim } from "./getClaim";
1+
import { getDecodedToken } from "./getDecodedToken";
22

33
/**
44
*
55
* @param keyName key to get from the token
66
* @returns { Promise<string | number | string[] | null> }
77
**/
88
export const getCurrentOrganization = async (): Promise<string | null> => {
9-
return (
10-
(await getClaim<{ org_code: string }, string>("org_code"))?.value || null
11-
);
9+
const decodedToken = await getDecodedToken();
10+
11+
if (!decodedToken) {
12+
return null;
13+
}
14+
15+
return decodedToken.org_code || decodedToken["x-hasura-org-code"];
1216
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, it, beforeEach } from "vitest";
2+
import { MemoryStorage, StorageKeys } from "../../sessionManager";
3+
import { setActiveStorage, getCurrentOrganization } from ".";
4+
import { createMockAccessToken } from "./testUtils";
5+
6+
const storage = new MemoryStorage();
7+
8+
describe("getCurrentOrganization", () => {
9+
beforeEach(() => {
10+
setActiveStorage(storage);
11+
});
12+
it("when no token", async () => {
13+
await storage.setSessionItem(StorageKeys.idToken, null);
14+
const idToken = await getCurrentOrganization();
15+
16+
expect(idToken).toStrictEqual(null);
17+
});
18+
19+
it("with access", async () => {
20+
await storage.setSessionItem(
21+
StorageKeys.accessToken,
22+
createMockAccessToken({
23+
org_code: null,
24+
["x-hasura-org-code"]: "org_123456",
25+
}),
26+
);
27+
const orgCode = await getCurrentOrganization();
28+
29+
expect(orgCode).toStrictEqual("org_123456");
30+
});
31+
});

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.test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,34 @@ describe("getFlag", () => {
1313
it("when no token", async () => {
1414
await storage.setSessionItem(StorageKeys.idToken, null);
1515
const idToken = await getFlag("test");
16+
expect(idToken).toStrictEqual(null);
17+
});
18+
19+
it("when no flags", async () => {
20+
await storage.setSessionItem(
21+
StorageKeys.accessToken,
22+
createMockAccessToken({
23+
feature_flags: null,
24+
}),
25+
);
26+
const idToken = await getFlag("test");
27+
28+
expect(idToken).toStrictEqual(null);
29+
});
30+
31+
it("when name missing", async () => {
32+
await storage.setSessionItem(
33+
StorageKeys.accessToken,
34+
createMockAccessToken({
35+
feature_flags: {
36+
test: {
37+
v: true,
38+
t: "b",
39+
},
40+
},
41+
}),
42+
);
43+
const idToken = await getFlag();
1644

1745
expect(idToken).toStrictEqual(null);
1846
});

lib/utils/token/getFlag.ts

+14-12
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
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();
1712

18-
if (name && flags) {
19-
const value = flags[name];
20-
return value ? value?.v : null;
13+
if (!claims) {
14+
return null;
2115
}
22-
return null;
16+
17+
const flags = claims.feature_flags || claims["x-hasura-feature-flags"];
18+
19+
if (!flags) {
20+
return null;
21+
}
22+
23+
const value = flags[name];
24+
return (value?.v as T) ?? null;
2325
};

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.accessToken, 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
};

0 commit comments

Comments
 (0)