Auth features (#64)
* Add useDashboardToken to app bridge module * Add authorization HOC * Fixes
This commit is contained in:
parent
0352356fd3
commit
864ca16f78
8 changed files with 158 additions and 24 deletions
11
package.json
11
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@saleor/app-sdk",
|
"name": "@saleor/app-sdk",
|
||||||
"version": "0.11.0",
|
"version": "0.12.0-1",
|
||||||
"description": "SDK for building great Saleor Apps",
|
"description": "SDK for building great Saleor Apps",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
@ -67,8 +67,8 @@
|
||||||
"watchlist": "^0.3.1",
|
"watchlist": "^0.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"@types/react": "17.0.49",
|
"@types/react": "18.0.21",
|
||||||
"@types/react-dom": "^17.0.11"
|
"@types/react-dom": "^18.0.5"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,tsx}": "eslint --cache --fix",
|
"*.{js,ts,tsx}": "eslint --cache --fix",
|
||||||
|
@ -101,6 +101,11 @@
|
||||||
"import": "./app-bridge/index.mjs",
|
"import": "./app-bridge/index.mjs",
|
||||||
"require": "./app-bridge/index.js"
|
"require": "./app-bridge/index.js"
|
||||||
},
|
},
|
||||||
|
"./util": {
|
||||||
|
"types": "./util/index.d.ts",
|
||||||
|
"import": "./util/index.mjs",
|
||||||
|
"require": "./util/index.js"
|
||||||
|
},
|
||||||
"./handlers/next": {
|
"./handlers/next": {
|
||||||
"types": "./handlers/next/index.d.ts",
|
"types": "./handlers/next/index.d.ts",
|
||||||
"import": "./handlers/next/index.mjs",
|
"import": "./handlers/next/index.mjs",
|
||||||
|
|
|
@ -6,8 +6,8 @@ specifiers:
|
||||||
'@types/debug': ^4.1.7
|
'@types/debug': ^4.1.7
|
||||||
'@types/node': ^18.7.15
|
'@types/node': ^18.7.15
|
||||||
'@types/node-fetch': ^2.6.2
|
'@types/node-fetch': ^2.6.2
|
||||||
'@types/react': 17.0.49
|
'@types/react': 18.0.21
|
||||||
'@types/react-dom': ^17.0.11
|
'@types/react-dom': ^18.0.5
|
||||||
'@types/uuid': ^8.3.4
|
'@types/uuid': ^8.3.4
|
||||||
'@typescript-eslint/eslint-plugin': ^5.36.1
|
'@typescript-eslint/eslint-plugin': ^5.36.1
|
||||||
'@typescript-eslint/parser': ^5.36.1
|
'@typescript-eslint/parser': ^5.36.1
|
||||||
|
@ -60,8 +60,8 @@ devDependencies:
|
||||||
'@types/debug': 4.1.7
|
'@types/debug': 4.1.7
|
||||||
'@types/node': 18.7.15
|
'@types/node': 18.7.15
|
||||||
'@types/node-fetch': 2.6.2
|
'@types/node-fetch': 2.6.2
|
||||||
'@types/react': 17.0.49
|
'@types/react': 18.0.21
|
||||||
'@types/react-dom': 17.0.17
|
'@types/react-dom': 18.0.6
|
||||||
'@types/uuid': 8.3.4
|
'@types/uuid': 8.3.4
|
||||||
'@typescript-eslint/eslint-plugin': 5.36.1_lbwfnm54o3pmr3ypeqp3btnera
|
'@typescript-eslint/eslint-plugin': 5.36.1_lbwfnm54o3pmr3ypeqp3btnera
|
||||||
'@typescript-eslint/parser': 5.36.1_yqf6kl63nyoq5megxukfnom5rm
|
'@typescript-eslint/parser': 5.36.1_yqf6kl63nyoq5megxukfnom5rm
|
||||||
|
@ -898,28 +898,14 @@ packages:
|
||||||
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/react-dom/17.0.17:
|
|
||||||
resolution: {integrity: sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg==}
|
|
||||||
dependencies:
|
|
||||||
'@types/react': 17.0.49
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/react-dom/18.0.6:
|
/@types/react-dom/18.0.6:
|
||||||
resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==}
|
resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 18.0.18
|
'@types/react': 18.0.21
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/react/17.0.49:
|
/@types/react/18.0.21:
|
||||||
resolution: {integrity: sha512-CCBPMZaPhcKkYUTqFs/hOWqKjPxhTEmnZWjlHHgIMop67DsXywf9B5Os9Hz8KSacjNOgIdnZVJamwl232uxoPg==}
|
resolution: {integrity: sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA==}
|
||||||
dependencies:
|
|
||||||
'@types/prop-types': 15.7.5
|
|
||||||
'@types/scheduler': 0.16.2
|
|
||||||
csstype: 3.1.0
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/react/18.0.18:
|
|
||||||
resolution: {integrity: sha512-6hI08umYs6NaiHFEEGioXnxJ+oEhY3eRz8VCUaudZmGdtvPviCJB8mgaMxaDWAdPSYd4eFavrPk2QIolwbLYrg==}
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/prop-types': 15.7.5
|
'@types/prop-types': 15.7.5
|
||||||
'@types/scheduler': 0.16.2
|
'@types/scheduler': 0.16.2
|
||||||
|
|
|
@ -6,6 +6,8 @@ export * from "./actions";
|
||||||
export * from "./app-bridge-provider";
|
export * from "./app-bridge-provider";
|
||||||
export * from "./events";
|
export * from "./events";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
export * from "./use-dashboard-token";
|
||||||
|
export * from "./with-authorization";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use new AppBridge(), createApp will be removed
|
* @deprecated use new AppBridge(), createApp will be removed
|
||||||
|
|
49
src/app-bridge/use-dashboard-token.ts
Normal file
49
src/app-bridge/use-dashboard-token.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import debugPkg from "debug";
|
||||||
|
import * as jose from "jose";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { useAppBridge } from "./app-bridge-provider";
|
||||||
|
|
||||||
|
export interface DashboardTokenPayload extends jose.JWTPayload {
|
||||||
|
app: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardTokenProps {
|
||||||
|
isTokenValid: boolean;
|
||||||
|
hasAppToken: boolean;
|
||||||
|
tokenClaims: DashboardTokenPayload | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const debug = debugPkg.debug("app-sdk:AppBridge:useDashboardToken");
|
||||||
|
|
||||||
|
export const useDashboardToken = (): DashboardTokenProps => {
|
||||||
|
const { appBridgeState } = useAppBridge();
|
||||||
|
|
||||||
|
const tokenClaims = useMemo(() => {
|
||||||
|
try {
|
||||||
|
if (appBridgeState?.token) {
|
||||||
|
debug("Trying to decode JWT token from dashboard");
|
||||||
|
return jose.decodeJwt(appBridgeState?.token) as DashboardTokenPayload;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debug("Failed decoding JWT token");
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [appBridgeState?.token]);
|
||||||
|
|
||||||
|
if (tokenClaims && !tokenClaims.iss) {
|
||||||
|
console.error(`
|
||||||
|
"iss" not found in decoded token claims. Ensure Saleor has domain assigned
|
||||||
|
Check documentation for more details
|
||||||
|
https://docs.saleor.io/docs/3.x/dashboard/configuration/site#general-information`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTokenValid = tokenClaims ? tokenClaims.iss === appBridgeState?.domain : false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isTokenValid,
|
||||||
|
tokenClaims,
|
||||||
|
hasAppToken: Boolean(appBridgeState?.token),
|
||||||
|
};
|
||||||
|
};
|
71
src/app-bridge/with-authorization.tsx
Normal file
71
src/app-bridge/with-authorization.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import * as React from "react";
|
||||||
|
import { PropsWithChildren, ReactNode } from "react";
|
||||||
|
|
||||||
|
import { isInIframe, useIsMounted } from "../util";
|
||||||
|
import { useDashboardToken } from "./use-dashboard-token";
|
||||||
|
|
||||||
|
function SimpleError({ children }: PropsWithChildren<{}>) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 32, color: "red" }}>
|
||||||
|
<p>{children}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
unmounted?: ReactNode;
|
||||||
|
notIframe?: ReactNode;
|
||||||
|
noDashboardToken?: ReactNode;
|
||||||
|
dashboardTokenInvalid?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps: Props = {
|
||||||
|
dashboardTokenInvalid: <SimpleError>Dashboard token is invalid</SimpleError>,
|
||||||
|
noDashboardToken: <SimpleError>Dashboard token doesn"t exist</SimpleError>,
|
||||||
|
notIframe: <SimpleError>The view can only be displayed inside iframe.</SimpleError>,
|
||||||
|
unmounted: <p>Loading</p>,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Most likely, views from your app will be only accessibly inside Dashboard iframe.
|
||||||
|
* This HOC can be used to handle all checks, with default messages included.
|
||||||
|
* Each error screen can be passed into HOC factory
|
||||||
|
*
|
||||||
|
* If screen can be accessible outside Dashboard - omit this HOC on this page
|
||||||
|
* */
|
||||||
|
export const withAuthorization =
|
||||||
|
(props: Props = defaultProps) =>
|
||||||
|
<BaseProps extends React.ComponentProps<NextPage>>(
|
||||||
|
BaseComponent: React.FunctionComponent<BaseProps>
|
||||||
|
) => {
|
||||||
|
const { dashboardTokenInvalid, noDashboardToken, notIframe, unmounted } = {
|
||||||
|
...defaultProps,
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
|
||||||
|
function AuthorizedPage(innerProps: BaseProps) {
|
||||||
|
const mounted = useIsMounted();
|
||||||
|
const { isTokenValid, hasAppToken } = useDashboardToken();
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return unmounted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInIframe()) {
|
||||||
|
return notIframe;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAppToken) {
|
||||||
|
return noDashboardToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTokenValid) {
|
||||||
|
return dashboardTokenInvalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BaseComponent {...innerProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthorizedPage;
|
||||||
|
};
|
2
src/util/index.ts
Normal file
2
src/util/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./is-in-iframe";
|
||||||
|
export * from "./use-is-mounted";
|
11
src/util/is-in-iframe.ts
Normal file
11
src/util/is-in-iframe.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export const isInIframe = () => {
|
||||||
|
if (!document || !window) {
|
||||||
|
throw new Error("isInIframe should be called only in browser");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return document.location !== window.parent.location;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
8
src/util/use-is-mounted.ts
Normal file
8
src/util/use-is-mounted.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export const useIsMounted = (): boolean => {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
|
|
||||||
|
return mounted;
|
||||||
|
};
|
Loading…
Reference in a new issue