Authenticated fetch update (#215)

* Remove MANAGE_APPS permission for available permission

* Fix missing header in useAuthenticatedFetch and add docs

* Update docs/protected-handlers.md

Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>

---------

Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>
This commit is contained in:
Lukasz Ostrowski 2023-03-14 12:31:11 +01:00 committed by GitHub
parent dfd632bb14
commit f7d38dc8d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 49 additions and 5 deletions

View file

@ -0,0 +1,5 @@
---
"@saleor/app-sdk": minor
---
Added useAuthenticatedFetch hook with can construct decorated window.fetch with pre-defined headers with required AppBridge state. Can be used with createProtectedHandler

View file

@ -0,0 +1,5 @@
---
"@saleor/app-sdk": patch
---
Remove MANAGE_APPS from possible permissions, because App should not have it. Mutations that requires MANAGE_APPS will not work with App Token even if permission is set

View file

@ -24,6 +24,7 @@ export type ProtectedHandlerContext = {
- the API URL has been registered, with help of the APL - the API URL has been registered, with help of the APL
- the request has `authorization-bearer` - the request has `authorization-bearer`
- the auth token is a valid JWT token created by the Saleor running on the given URL - the auth token is a valid JWT token created by the Saleor running on the given URL
- user has required permissions in the token
For example purposes our endpoint will only log welcome message: For example purposes our endpoint will only log welcome message:
@ -44,8 +45,10 @@ export const handler = async (
/** /**
* If any of the requirements is failed, an error response will be returned. * If any of the requirements is failed, an error response will be returned.
* Otherwise, provided handler function fill be called. * Otherwise, provided handler function fill be called.
*
* Last argument is optional array of permissions that will be checked. If user doesn't have them, will return 401 before handler is called
*/ */
export default createProtectedHandler(handler, saleorApp.apl); export default createProtectedHandler(handler, saleorApp.apl, ["MANAGE_ORDERS"]);
``` ```
To make your requests successfully communicate with the backend, `saleor-api-url` and `authorization-bearer` headers are required: To make your requests successfully communicate with the backend, `saleor-api-url` and `authorization-bearer` headers are required:
@ -65,3 +68,30 @@ fetch("/api/protected", {
``` ```
If you want to read more about `appBridgeState`, check [App Bridge](./app-bridge.md) documentation. If you want to read more about `appBridgeState`, check [App Bridge](./app-bridge.md) documentation.
### Using `useAuthenticatedFetch()` hook
Instead of manually attaching headers with AppBridge context, you can use `useAuthenticatedFetch()` hook
Since it requires AppBridge, it's only available in browser context. It depends on `Window` object,
so your app will break if Next.js tries to render it server-side. Hence, ensure component that uses the hook is imported with dynamic()
Component must be within `AppBridgeProvider` to have access to the AppBridge
```tsx
import { useAuthenticatedFetch } from "@saleor/app-sdk/app-bridge";
import { useEffect } from "react";
export const ClientComponent = () => {
const fetch = useAuthenticatedFetch();
useEffect(() => {
/**
* Auth headers are set up automatically, so you can just call the fetch function
*/
fetch("/api/protected");
}, [fetch]);
return <div>Your UI</div>;
};
```

View file

@ -1,4 +1,8 @@
import { SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_DOMAIN_HEADER } from "../const"; import {
SALEOR_API_URL_HEADER,
SALEOR_AUTHORIZATION_BEARER_HEADER,
SALEOR_DOMAIN_HEADER,
} from "../const";
import { AppBridge } from "./app-bridge"; import { AppBridge } from "./app-bridge";
import { useAppBridge } from "./app-bridge-provider"; import { useAppBridge } from "./app-bridge-provider";
@ -10,11 +14,12 @@ type HasAppBridgeState = Pick<AppBridge, "getState">;
export const createAuthenticatedFetch = export const createAuthenticatedFetch =
(appBridge: HasAppBridgeState, fetch = global.fetch): typeof global.fetch => (appBridge: HasAppBridgeState, fetch = global.fetch): typeof global.fetch =>
(input, init) => { (input, init) => {
const { token, domain } = appBridge.getState(); const { token, domain, saleorApiUrl } = appBridge.getState();
const headers = new Headers(init?.headers); const headers = new Headers(init?.headers);
headers.set(SALEOR_DOMAIN_HEADER, domain); headers.set(SALEOR_DOMAIN_HEADER, domain);
headers.set(SALEOR_AUTHORIZATION_BEARER_HEADER, token ?? ""); headers.set(SALEOR_AUTHORIZATION_BEARER_HEADER, token ?? "");
headers.set(SALEOR_API_URL_HEADER, saleorApiUrl ?? "");
const clonedInit: RequestInit = { const clonedInit: RequestInit = {
...(init ?? {}), ...(init ?? {}),
@ -27,7 +32,7 @@ export const createAuthenticatedFetch =
/** /**
* Hook working only in browser context. Ensure parent component is dynamic() and mounted in the browser. * Hook working only in browser context. Ensure parent component is dynamic() and mounted in the browser.
*/ */
export const useAuthenticatedFetch = (fetch = global.fetch) => { export const useAuthenticatedFetch = (fetch = window.fetch): typeof window.fetch => {
const { appBridge } = useAppBridge(); const { appBridge } = useAppBridge();
if (!appBridge) { if (!appBridge) {

View file

@ -22,7 +22,6 @@ export type Permission =
| "MANAGE_USERS" | "MANAGE_USERS"
| "MANAGE_STAFF" | "MANAGE_STAFF"
| "IMPERSONATE_USER" | "IMPERSONATE_USER"
| "MANAGE_APPS"
| "MANAGE_OBSERVABILITY" | "MANAGE_OBSERVABILITY"
| "MANAGE_CHECKOUTS" | "MANAGE_CHECKOUTS"
| "HANDLE_CHECKOUTS" | "HANDLE_CHECKOUTS"