Skip to content

Commit

Permalink
feat: more helpers and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielRivers committed Sep 16, 2024
1 parent 2221169 commit f3d8331
Show file tree
Hide file tree
Showing 15 changed files with 457 additions and 31 deletions.
23 changes: 23 additions & 0 deletions lib/utils/token/getClaim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { JWTDecoded } from "@kinde/jwt-decoder";
import { getDecodedToken } from "./getDecodedToken";

/**
*
* @param keyName key to get from the token
* @returns { Promise<string | number | string[] | null> }
*/
export const getClaim = async <T = JWTDecoded, V = string | number | string[]>(
keyName: keyof T,
): Promise<{
name: keyof T;
value: V;
} | null> => {
const claims = await getDecodedToken<T>("accessToken");
if (!claims) {
return null;
}
return {
name: keyName,
value: claims[keyName] as V,
};
};
10 changes: 10 additions & 0 deletions lib/utils/token/getClaims.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { JWTDecoded } from "@kinde/jwt-decoder";
import { getDecodedToken } from "./getDecodedToken";

/**
* get all claims from the token
* @returns { Promise<T | null> }
*/
export const getClaims = async <T = JWTDecoded>(): Promise<T | null> => {
return getDecodedToken<T>("accessToken");
};
12 changes: 12 additions & 0 deletions lib/utils/token/getCurrentOrganization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getClaim } from "./getClaim";

/**
*
* @param keyName key to get from the token
* @returns { Promise<string | number | string[] | null> }
**/
export const getCurrentOrganization = async (): Promise<string | null> => {
return (
(await getClaim<{ org_code: string }, string>("org_code"))?.value || null
);
};
5 changes: 1 addition & 4 deletions lib/utils/token/getDecodedToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,11 @@ export const getDecodedToken = async <
tokenType: "accessToken" | "idToken" = "accessToken",
): Promise<T | null> => {
const activeStorage = getActiveStorage();
if (!activeStorage) {
throw new Error("Session manager is not initialized");
}

const token = (await activeStorage.getSessionItem(
tokenType === "accessToken" ? StorageKeys.accessToken : StorageKeys.idToken,
)) as string;
console.log("token", token);

if (!token) {
return null;
}
Expand Down
62 changes: 62 additions & 0 deletions lib/utils/token/getFlag.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it, beforeEach } from "vitest";
import { MemoryStorage, StorageKeys } from "../../sessionManager";
import { setActiveStorage, getFlag } from ".";
import { createMockAccessToken } from "./testUtils";

const storage = new MemoryStorage();

describe("getFlag", () => {
beforeEach(() => {
setActiveStorage(storage);
});
it("when no token", async () => {
await storage.setSessionItem(StorageKeys.idToken, null);
const idToken = await getFlag("test");

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

it("boolean", async () => {
await storage.setSessionItem(StorageKeys.accessToken, createMockAccessToken({ feature_flags: {
test: {
v: true,
t: "b"
}}}),);
const idToken = await getFlag<boolean>("test");

expect(idToken).toStrictEqual(true);
});

it("string", async () => {
await storage.setSessionItem(StorageKeys.accessToken, createMockAccessToken({ feature_flags: {
test: {
v: "hello",
t: "s"
}}}),);
const idToken = await getFlag<string>("test");

expect(idToken).toStrictEqual("hello");
});

it("integer", async () => {
await storage.setSessionItem(StorageKeys.accessToken, createMockAccessToken({ feature_flags: {
test: {
v: 5,
t: "i"
}}}),);
const idToken = await getFlag<number>("test");

expect(idToken).toStrictEqual(5);
});

it("no existing flag", async () => {
await storage.setSessionItem(StorageKeys.accessToken, createMockAccessToken({ feature_flags: {
test: {
v: 5,
t: "i"
}}}),);
const idToken = await getFlag<number>("noexist");

expect(idToken).toStrictEqual(null);
});
});
23 changes: 23 additions & 0 deletions lib/utils/token/getFlag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getClaim } from "./getClaim";

/**
*
* @param keyName key to get from the token
* @returns { Promise<string | number | string[] | null> }
*/
export const getFlag = async <T = string | boolean | number>(
name: string,
): Promise<T | null> => {
const flags = (
await getClaim<
{ feature_flags: string },
Record<string, { t: "b" | "i" | "s"; v: T }>
>("feature_flags")
)?.value;

if (name && flags) {
const value = flags[name];
return value?.v || null;
}
return null;
};
82 changes: 82 additions & 0 deletions lib/utils/token/getPermission.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, expect, it, beforeEach } from "vitest";
import { MemoryStorage, StorageKeys } from "../../sessionManager";
import { setActiveStorage, getPermission } from ".";
import { createMockAccessToken } from "./testUtils";

const storage = new MemoryStorage();

enum PermissionEnum {
canEdit = "canEdit",
}

describe("getPermission", () => {
beforeEach(() => {
setActiveStorage(storage);
});
it("when no token", async () => {
await storage.setSessionItem(StorageKeys.idToken, null);
const idToken = await getPermission("test");

expect(idToken).toStrictEqual({
isGranted: false,
orgCode: null,
permissionKey: "test",
});
});

it("when no token with enum", async () => {
await storage.setSessionItem(StorageKeys.idToken, null);
const idToken = await getPermission<PermissionEnum>(PermissionEnum.canEdit);

expect(idToken).toStrictEqual({
isGranted: false,
orgCode: null,
permissionKey: "canEdit",
});
});

it("with access", async () => {
await storage.setSessionItem(
StorageKeys.accessToken,
createMockAccessToken({ permissions: ["canEdit"] }),
);
const idToken = await getPermission<PermissionEnum>(PermissionEnum.canEdit);

expect(idToken).toStrictEqual({
isGranted: true,
orgCode: "org_123456789",
permissionKey: "canEdit",
});
});

it("with access different org", async () => {
await storage.setSessionItem(
StorageKeys.accessToken,
createMockAccessToken({
permissions: ["canEdit"],
org_code: "org_123456799",
}),
);
const idToken = await getPermission<PermissionEnum>(PermissionEnum.canEdit);

expect(idToken).toStrictEqual({
isGranted: true,
orgCode: "org_123456799",
permissionKey: "canEdit",
});
});

it("no access, empty permission array", async () => {
await storage.setSessionItem(
StorageKeys.accessToken,
createMockAccessToken({ permissions: null }),
);
const idToken = await getPermission<PermissionEnum>(PermissionEnum.canEdit);

expect(idToken).toStrictEqual({
isGranted: false,
orgCode: "org_123456789",
permissionKey: "canEdit",
});
});
});
33 changes: 33 additions & 0 deletions lib/utils/token/getPermission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getDecodedToken } from ".";

export type PermissionAccess = {
permissionKey: string;
orgCode: string | null;
isGranted: boolean;
};

/**
*
* @param permissionKey gets the value of a permission
* @returns { PermissionAccess }
*/
export const getPermission = async <T = string>(
permissionKey: T,
): Promise<PermissionAccess> => {
const token = await getDecodedToken();

if (!token) {
return {
permissionKey: permissionKey as string,
orgCode: null,
isGranted: false,
};
}

const permissions = token.permissions || [];
return {
permissionKey: permissionKey as string,
orgCode: token.org_code,
isGranted: !!permissions.includes(permissionKey as string),
};
};
66 changes: 66 additions & 0 deletions lib/utils/token/getPermissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, it, beforeEach } from "vitest";
import { MemoryStorage, StorageKeys } from "../../sessionManager";
import { setActiveStorage } from ".";
import { createMockAccessToken } from "./testUtils";
import { getPermissions } from ".";

const storage = new MemoryStorage();

enum PermissionEnum {
canEdit = "canEdit",
}

describe("getPermissions", () => {
beforeEach(() => {
setActiveStorage(storage);
});

it("when no token", async () => {
await storage.setSessionItem(StorageKeys.idToken, null);
const idToken = await getPermissions();

expect(idToken).toStrictEqual({
orgCode: null,
permissions: [],
});
});

it("with value", async () => {
await storage.setSessionItem(
StorageKeys.accessToken,
createMockAccessToken({ permissions: ["canEdit"] }),
);
const idToken = await getPermissions();

expect(idToken).toStrictEqual({
orgCode: "org_123456789",
permissions: ["canEdit"],
});
});

it("with value and typed permissions", async () => {
await storage.setSessionItem(
StorageKeys.accessToken,
createMockAccessToken({ permissions: ["canEdit"] }),
);
const idToken = await getPermissions<PermissionEnum>();

expect(idToken).toStrictEqual({
orgCode: "org_123456789",
permissions: [PermissionEnum.canEdit],
});
});

it("no permissions array", async () => {
await storage.setSessionItem(
StorageKeys.accessToken,
createMockAccessToken({ permissions: null }),
);
const idToken = await getPermissions<PermissionEnum>();

expect(idToken).toStrictEqual({
orgCode: "org_123456789",
permissions: [],
});
});
});
23 changes: 23 additions & 0 deletions lib/utils/token/getPermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getDecodedToken } from ".";

export type Permissions<T> = { orgCode: string | null; permissions: T[] };
/**
* Get all permissions
* @returns { Promise<Permissions> }
*/
export const getPermissions = async <T = string>(): Promise<Permissions<T>> => {
const token = await getDecodedToken();

if (!token) {
return {
orgCode: null,
permissions: [],
};
}

const permissions = token.permissions || [];
return {
orgCode: token.org_code,
permissions: permissions as T[],
};
};
Loading

0 comments on commit f3d8331

Please sign in to comment.