Auth features (#64)

* Add useDashboardToken to app bridge module

* Add authorization HOC

* Fixes
This commit is contained in:
Lukasz Ostrowski 2022-10-05 13:12:19 +02:00 committed by GitHub
parent 0352356fd3
commit 864ca16f78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 158 additions and 24 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@saleor/app-sdk",
"version": "0.11.0",
"version": "0.12.0-1",
"description": "SDK for building great Saleor Apps",
"types": "index.d.ts",
"main": "index.js",
@ -67,8 +67,8 @@
"watchlist": "^0.3.1",
"react": "^18.2.0",
"react-dom": "18.2.0",
"@types/react": "17.0.49",
"@types/react-dom": "^17.0.11"
"@types/react": "18.0.21",
"@types/react-dom": "^18.0.5"
},
"lint-staged": {
"*.{js,ts,tsx}": "eslint --cache --fix",
@ -101,6 +101,11 @@
"import": "./app-bridge/index.mjs",
"require": "./app-bridge/index.js"
},
"./util": {
"types": "./util/index.d.ts",
"import": "./util/index.mjs",
"require": "./util/index.js"
},
"./handlers/next": {
"types": "./handlers/next/index.d.ts",
"import": "./handlers/next/index.mjs",

View file

@ -6,8 +6,8 @@ specifiers:
'@types/debug': ^4.1.7
'@types/node': ^18.7.15
'@types/node-fetch': ^2.6.2
'@types/react': 17.0.49
'@types/react-dom': ^17.0.11
'@types/react': 18.0.21
'@types/react-dom': ^18.0.5
'@types/uuid': ^8.3.4
'@typescript-eslint/eslint-plugin': ^5.36.1
'@typescript-eslint/parser': ^5.36.1
@ -60,8 +60,8 @@ devDependencies:
'@types/debug': 4.1.7
'@types/node': 18.7.15
'@types/node-fetch': 2.6.2
'@types/react': 17.0.49
'@types/react-dom': 17.0.17
'@types/react': 18.0.21
'@types/react-dom': 18.0.6
'@types/uuid': 8.3.4
'@typescript-eslint/eslint-plugin': 5.36.1_lbwfnm54o3pmr3ypeqp3btnera
'@typescript-eslint/parser': 5.36.1_yqf6kl63nyoq5megxukfnom5rm
@ -898,28 +898,14 @@ packages:
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
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:
resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==}
dependencies:
'@types/react': 18.0.18
'@types/react': 18.0.21
dev: true
/@types/react/17.0.49:
resolution: {integrity: sha512-CCBPMZaPhcKkYUTqFs/hOWqKjPxhTEmnZWjlHHgIMop67DsXywf9B5Os9Hz8KSacjNOgIdnZVJamwl232uxoPg==}
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==}
/@types/react/18.0.21:
resolution: {integrity: sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA==}
dependencies:
'@types/prop-types': 15.7.5
'@types/scheduler': 0.16.2

View file

@ -6,6 +6,8 @@ export * from "./actions";
export * from "./app-bridge-provider";
export * from "./events";
export * from "./types";
export * from "./use-dashboard-token";
export * from "./with-authorization";
/**
* @deprecated use new AppBridge(), createApp will be removed

View 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),
};
};

View 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&quot;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
View file

@ -0,0 +1,2 @@
export * from "./is-in-iframe";
export * from "./use-is-mounted";

11
src/util/is-in-iframe.ts Normal file
View 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;
}
};

View file

@ -0,0 +1,8 @@
import { useEffect, useState } from "react";
export const useIsMounted = (): boolean => {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return mounted;
};