Add user email and permissions to app bridge (#167)
* Add user email and permissions to app bridge * Update src/app-bridge/app-bridge-state.ts Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com> * Fix permissions name --------- Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>
This commit is contained in:
parent
450a3ce741
commit
7413de221b
6 changed files with 118 additions and 15 deletions
|
@ -1,4 +1,5 @@
|
|||
import { LocaleCode } from "../locales";
|
||||
import { Permission } from "../types";
|
||||
import { ThemeType } from "./events";
|
||||
|
||||
export type AppBridgeState = {
|
||||
|
@ -10,6 +11,17 @@ export type AppBridgeState = {
|
|||
theme: ThemeType;
|
||||
locale: LocaleCode;
|
||||
saleorApiUrl: string;
|
||||
user?: {
|
||||
/**
|
||||
* Original permissions of the user that is using the app.
|
||||
* *Not* the same permissions as the app itself.
|
||||
*
|
||||
* Can be used by app to check if user is authorized to perform
|
||||
* domain specific actions
|
||||
*/
|
||||
permissions: Permission[];
|
||||
email: string;
|
||||
};
|
||||
};
|
||||
|
||||
type Options = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { fireEvent } from "@testing-library/dom";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { LocaleCode } from "../locales";
|
||||
// eslint-disable-next-line
|
||||
|
@ -15,6 +15,8 @@ import {
|
|||
// mock document.referrer
|
||||
const origin = "http://example.com";
|
||||
const domain = "saleor.domain.host";
|
||||
const validJwtToken =
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6Ijk4ZTEzNDk4YmM5NThjM2QyNzk2NjY5Zjk0NzYxMzZkIn0.eyJpYXQiOjE2NjkxOTE4NDUsIm93bmVyIjoic2FsZW9yIiwiaXNzIjoiZGVtby5ldS5zYWxlb3IuY2xvdWQiLCJleHAiOjE2NjkyNzgyNDUsInRva2VuIjoic2JsRmVrWnVCSUdXIiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsInR5cGUiOiJ0aGlyZHBhcnR5IiwidXNlcl9pZCI6IlZYTmxjam95TWc9PSIsImlzX3N0YWZmIjp0cnVlLCJhcHAiOiJRWEJ3T2pJM05RPT0iLCJwZXJtaXNzaW9ucyI6W10sInVzZXJfcGVybWlzc2lvbnMiOlsiTUFOQUdFX1BBR0VfVFlQRVNfQU5EX0FUVFJJQlVURVMiLCJNQU5BR0VfUFJPRFVDVF9UWVBFU19BTkRfQVRUUklCVVRFUyIsIk1BTkFHRV9ESVNDT1VOVFMiLCJNQU5BR0VfUExVR0lOUyIsIk1BTkFHRV9TVEFGRiIsIk1BTkFHRV9QUk9EVUNUUyIsIk1BTkFHRV9TSElQUElORyIsIk1BTkFHRV9UUkFOU0xBVElPTlMiLCJNQU5BR0VfT0JTRVJWQUJJTElUWSIsIk1BTkFHRV9VU0VSUyIsIk1BTkFHRV9BUFBTIiwiTUFOQUdFX0NIQU5ORUxTIiwiTUFOQUdFX0dJRlRfQ0FSRCIsIkhBTkRMRV9QQVlNRU5UUyIsIklNUEVSU09OQVRFX1VTRVIiLCJNQU5BR0VfU0VUVElOR1MiLCJNQU5BR0VfUEFHRVMiLCJNQU5BR0VfTUVOVVMiLCJNQU5BR0VfQ0hFQ0tPVVRTIiwiSEFORExFX0NIRUNLT1VUUyIsIk1BTkFHRV9PUkRFUlMiXX0.PUyvuUlDvUBXMGSaexusdlkY5wF83M8tsjefVXOknaKuVgLbafvLOgx78YGVB4kdAybC7O3Yjs7IIFOzz5U80Q";
|
||||
|
||||
Object.defineProperty(window.document, "referrer", {
|
||||
value: origin,
|
||||
|
@ -30,7 +32,7 @@ Object.defineProperty(window, "location", {
|
|||
|
||||
const handshakeEvent: HandshakeEvent = {
|
||||
payload: {
|
||||
token: "mock-token",
|
||||
token: validJwtToken,
|
||||
version: 1,
|
||||
},
|
||||
type: "handshake",
|
||||
|
@ -76,11 +78,20 @@ describe("AppBridge", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
appBridge = new AppBridge();
|
||||
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {
|
||||
// noop
|
||||
});
|
||||
|
||||
vi.spyOn(console, "error").mockImplementation(() => {
|
||||
// noop
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("correctly sets the default domain, if not set in constructor", () => {
|
||||
expect(appBridge.getState().domain).toEqual(domain);
|
||||
});
|
||||
|
@ -88,7 +99,7 @@ describe("AppBridge", () => {
|
|||
it("authenticates", () => {
|
||||
expect(appBridge.getState().ready).toBe(false);
|
||||
|
||||
const token = "test-token";
|
||||
const token = validJwtToken;
|
||||
fireEvent(
|
||||
window,
|
||||
new MessageEvent("message", {
|
||||
|
@ -107,7 +118,7 @@ describe("AppBridge", () => {
|
|||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
const token = "fresh-token";
|
||||
const token = validJwtToken;
|
||||
|
||||
// First call proper event
|
||||
fireEvent(
|
||||
|
@ -146,13 +157,13 @@ describe("AppBridge", () => {
|
|||
fireEvent(
|
||||
window,
|
||||
new MessageEvent("message", {
|
||||
data: { type: "handshake", payload: { token: "123" } },
|
||||
data: { type: "handshake", payload: { token: validJwtToken } },
|
||||
origin,
|
||||
})
|
||||
);
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(appBridge.getState().token).toEqual("123");
|
||||
expect(appBridge.getState().token).toEqual(validJwtToken);
|
||||
});
|
||||
|
||||
it("Subscribes to theme change event and runs callback with new value after delay", async () => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import debugPkg from "debug";
|
||||
|
||||
import { LocaleCode } from "../locales";
|
||||
import { extractUserFromJwt } from "../util/extract-user-from-jwt";
|
||||
import { Actions, actions } from "./actions";
|
||||
import { AppBridgeState, AppBridgeStateContainer } from "./app-bridge-state";
|
||||
import { AppIframeParams } from "./app-iframe-params";
|
||||
|
@ -21,10 +22,16 @@ function eventStateReducer(state: AppBridgeState, event: Events) {
|
|||
|
||||
switch (event.type) {
|
||||
case EventType.handshake: {
|
||||
const userJwtPayload = extractUserFromJwt(event.payload.token);
|
||||
|
||||
return {
|
||||
...state,
|
||||
ready: true,
|
||||
token: event.payload.token,
|
||||
user: {
|
||||
email: userJwtPayload.email,
|
||||
permissions: userJwtPayload.userPermissions,
|
||||
},
|
||||
};
|
||||
}
|
||||
case EventType.redirect: {
|
||||
|
|
29
src/types.ts
29
src/types.ts
|
@ -14,25 +14,36 @@ export type AppExtensionMount =
|
|||
| "ORDER_OVERVIEW_CREATE"
|
||||
| "ORDER_OVERVIEW_MORE_ACTIONS";
|
||||
|
||||
export type AppPermission =
|
||||
| "HANDLE_CHECKOUTS"
|
||||
| "HANDLE_PAYMENTS"
|
||||
| "HANDLE_TAXES"
|
||||
/**
|
||||
* TODO: Extract from Saleor graphQL schema
|
||||
* Reference https://docs.saleor.io/docs/3.x/api-reference/enums/permission-enum
|
||||
*/
|
||||
export type Permission =
|
||||
| "MANAGE_USERS"
|
||||
| "MANAGE_STAFF"
|
||||
| "IMPERSONATE_USER"
|
||||
| "MANAGE_APPS"
|
||||
| "MANAGE_OBSERVABILITY"
|
||||
| "MANAGE_CHECKOUTS"
|
||||
| "HANDLE_CHECKOUTS"
|
||||
| "HANDLE_TAXES"
|
||||
| "MANAGE_TAXES"
|
||||
| "MANAGE_CHANNELS"
|
||||
| "MANAGE_DISCOUNTS"
|
||||
| "MANAGE_GIFT_CARD"
|
||||
| "MANAGE_MENUS"
|
||||
| "MANAGE_ORDERS"
|
||||
| "MANAGE_PAGES"
|
||||
| "MANAGE_PAGE_TYPES_AND_ATTRIBUTES"
|
||||
| "HANDLE_PAYMENTS"
|
||||
| "MANAGE_PLUGINS"
|
||||
| "MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES"
|
||||
| "MANAGE_PRODUCTS"
|
||||
| "MANAGE_SETTINGS"
|
||||
| "MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES"
|
||||
| "MANAGE_SHIPPING"
|
||||
| "MANAGE_STAFF"
|
||||
| "MANAGE_TRANSLATIONS"
|
||||
| "MANAGE_USERS";
|
||||
| "MANAGE_SETTINGS"
|
||||
| "MANAGE_TRANSLATIONS";
|
||||
|
||||
export type AppPermission = Permission;
|
||||
|
||||
export type WebhookEvent = string;
|
||||
|
||||
|
|
43
src/util/extract-user-from-jwt.test.ts
Normal file
43
src/util/extract-user-from-jwt.test.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { extractUserFromJwt } from "./extract-user-from-jwt";
|
||||
|
||||
const validJwtToken =
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6Ijk4ZTEzNDk4YmM5NThjM2QyNzk2NjY5Zjk0NzYxMzZkIn0.eyJpYXQiOjE2NjkxOTE4NDUsIm93bmVyIjoic2FsZW9yIiwiaXNzIjoiZGVtby5ldS5zYWxlb3IuY2xvdWQiLCJleHAiOjE2NjkyNzgyNDUsInRva2VuIjoic2JsRmVrWnVCSUdXIiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsInR5cGUiOiJ0aGlyZHBhcnR5IiwidXNlcl9pZCI6IlZYTmxjam95TWc9PSIsImlzX3N0YWZmIjp0cnVlLCJhcHAiOiJRWEJ3T2pJM05RPT0iLCJwZXJtaXNzaW9ucyI6W10sInVzZXJfcGVybWlzc2lvbnMiOlsiTUFOQUdFX1BBR0VfVFlQRVNfQU5EX0FUVFJJQlVURVMiLCJNQU5BR0VfUFJPRFVDVF9UWVBFU19BTkRfQVRUUklCVVRFUyIsIk1BTkFHRV9ESVNDT1VOVFMiLCJNQU5BR0VfUExVR0lOUyIsIk1BTkFHRV9TVEFGRiIsIk1BTkFHRV9QUk9EVUNUUyIsIk1BTkFHRV9TSElQUElORyIsIk1BTkFHRV9UUkFOU0xBVElPTlMiLCJNQU5BR0VfT0JTRVJWQUJJTElUWSIsIk1BTkFHRV9VU0VSUyIsIk1BTkFHRV9BUFBTIiwiTUFOQUdFX0NIQU5ORUxTIiwiTUFOQUdFX0dJRlRfQ0FSRCIsIkhBTkRMRV9QQVlNRU5UUyIsIklNUEVSU09OQVRFX1VTRVIiLCJNQU5BR0VfU0VUVElOR1MiLCJNQU5BR0VfUEFHRVMiLCJNQU5BR0VfTUVOVVMiLCJNQU5BR0VfQ0hFQ0tPVVRTIiwiSEFORExFX0NIRUNLT1VUUyIsIk1BTkFHRV9PUkRFUlMiXX0.PUyvuUlDvUBXMGSaexusdlkY5wF83M8tsjefVXOknaKuVgLbafvLOgx78YGVB4kdAybC7O3Yjs7IIFOzz5U80Q";
|
||||
|
||||
const invalidToken = "foo";
|
||||
|
||||
describe("extractUserFromJwt", () => {
|
||||
it("Throws if token is invalid", () => {
|
||||
expect(() => extractUserFromJwt(invalidToken)).toThrow();
|
||||
});
|
||||
|
||||
it("Extracts email and user permissions from the token", () => {
|
||||
expect(extractUserFromJwt(validJwtToken)).toEqual({
|
||||
email: "admin@example.com",
|
||||
userPermissions: [
|
||||
"MANAGE_PAGE_TYPES_AND_ATTRIBUTES",
|
||||
"MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES",
|
||||
"MANAGE_DISCOUNTS",
|
||||
"MANAGE_PLUGINS",
|
||||
"MANAGE_STAFF",
|
||||
"MANAGE_PRODUCTS",
|
||||
"MANAGE_SHIPPING",
|
||||
"MANAGE_TRANSLATIONS",
|
||||
"MANAGE_OBSERVABILITY",
|
||||
"MANAGE_USERS",
|
||||
"MANAGE_APPS",
|
||||
"MANAGE_CHANNELS",
|
||||
"MANAGE_GIFT_CARD",
|
||||
"HANDLE_PAYMENTS",
|
||||
"IMPERSONATE_USER",
|
||||
"MANAGE_SETTINGS",
|
||||
"MANAGE_PAGES",
|
||||
"MANAGE_MENUS",
|
||||
"MANAGE_CHECKOUTS",
|
||||
"HANDLE_CHECKOUTS",
|
||||
"MANAGE_ORDERS",
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
19
src/util/extract-user-from-jwt.ts
Normal file
19
src/util/extract-user-from-jwt.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import * as jose from "jose";
|
||||
|
||||
import { AppPermission } from "../types";
|
||||
|
||||
type TokenUserPayload = {
|
||||
email: string;
|
||||
userPermissions: AppPermission[];
|
||||
};
|
||||
|
||||
export const extractUserFromJwt = (jwtToken: string): TokenUserPayload => {
|
||||
const tokenDecoded = jose.decodeJwt(jwtToken);
|
||||
|
||||
const { email, user_permissions: userPermissions } = tokenDecoded;
|
||||
|
||||
return {
|
||||
email,
|
||||
userPermissions,
|
||||
} as TokenUserPayload;
|
||||
};
|
Loading…
Reference in a new issue