diff --git a/tool-plugins/nextjs-starter/README.md b/tool-plugins/nextjs-starter/README.md
index 9c27d0e..5400c9b 100644
--- a/tool-plugins/nextjs-starter/README.md
+++ b/tool-plugins/nextjs-starter/README.md
@@ -46,7 +46,7 @@ There are two ways on how you can create a tool inside Storyblok. Depending on y
4. Select `tool` as extension type
5. Click on **Save**
-### Tool Configuration
+### Configuration
Once the tool has been created, a new entry will appear inside the extension list. Open it and navigate to the `OAuth 2.0 and Pages` tab.
@@ -70,6 +70,43 @@ Start the application by running:
yarn dev # pnpm dev or npm run dev
```
+### App Bridge
+
+App Bridge is an extra authentication layer recently introduced for Space Plugins and Tool Plugins. This starter assumes you've enabled App Bridge on the Settings page. Documentation on App Bridge will come in the near future, but you don't need to know about its inner process. This starter addresses a large portion of this aspect out of the box.
+
+
+
+If you don't want to use App Bridge, you can use [the legacy template](https://github.com/storyblok/custom-app-examples/tree/main/app-nextjs-starter).
+
+### App Bridge in Depth
+
+App Bridge authentication starts on the frontend by sending a postMessage to `app.storyblok.com`. In the `src/pages/index.tsx` file, you can find the following code:
+
+```jsx
+const { completed } = useAppBridge({ type: 'space-plugin', oauth: true });
+
+return (
+
+ {completed && (
+
+
+
+
+ )}
+
+);
+```
+
+The code above handles both App Bridge authentication and OAuth.
+
+1. If you need to use Storyblok's Management API:
+
+After completing both authentications, the `` component is rendered. This component sends a request to `/api/user_info`. The OAuth token is automatically included in the request as a cookie, and the endpoint retrieves the session using `await getAppSession(req, res)`. It then fetches user information from Storyblok's Management API using the OAuth token.
+
+2. If you don't need the Management API but still want to validate the request on the backend:
+
+When the `` component is rendered, it makes a request to `/api/test`. We attach the App Bridge token as a header. The endpoint verifies the token using `await verifyAppBridgeHeader(req)`. Only if the token is verified can you perform any desired action.
+
### Tool Installation
Finally, install the application to your space:
diff --git a/tool-plugins/nextjs-starter/docs/app-bridge.png b/tool-plugins/nextjs-starter/docs/app-bridge.png
new file mode 100644
index 0000000..3dc2e20
Binary files /dev/null and b/tool-plugins/nextjs-starter/docs/app-bridge.png differ
diff --git a/tool-plugins/nextjs-starter/package.json b/tool-plugins/nextjs-starter/package.json
index 4a05a95..6207e78 100644
--- a/tool-plugins/nextjs-starter/package.json
+++ b/tool-plugins/nextjs-starter/package.json
@@ -9,13 +9,15 @@
"lint": "next lint"
},
"dependencies": {
- "@storyblok/app-extension-auth": "1.0.3",
+ "@storyblok/app-extension-auth": "2.0.0",
+ "jsonwebtoken": "^9.0.2",
"next": "13.4.19",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "5.2.2"
},
"devDependencies": {
+ "@types/jsonwebtoken": "^9.0.7",
"@types/node": "20.5.9",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7"
diff --git a/tool-plugins/nextjs-starter/pnpm-lock.yaml b/tool-plugins/nextjs-starter/pnpm-lock.yaml
deleted file mode 100644
index cf3eb0c..0000000
--- a/tool-plugins/nextjs-starter/pnpm-lock.yaml
+++ /dev/null
@@ -1,448 +0,0 @@
-lockfileVersion: '6.0'
-
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
-
-dependencies:
- '@storyblok/app-extension-auth':
- specifier: 1.0.0-alpha.1
- version: 1.0.0-alpha.1
- next:
- specifier: 13.4.19
- version: 13.4.19(react-dom@18.2.0)(react@18.2.0)
- react:
- specifier: 18.2.0
- version: 18.2.0
- react-dom:
- specifier: 18.2.0
- version: 18.2.0(react@18.2.0)
- typescript:
- specifier: 5.2.2
- version: 5.2.2
-
-devDependencies:
- '@types/node':
- specifier: 20.5.9
- version: 20.5.9
- '@types/react':
- specifier: 18.2.21
- version: 18.2.21
- '@types/react-dom':
- specifier: 18.2.7
- version: 18.2.7
-
-packages:
-
- /@next/env@13.4.19:
- resolution: {integrity: sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ==}
- dev: false
-
- /@next/swc-darwin-arm64@13.4.19:
- resolution: {integrity: sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [darwin]
- requiresBuild: true
- dev: false
- optional: true
-
- /@next/swc-darwin-x64@13.4.19:
- resolution: {integrity: sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [darwin]
- requiresBuild: true
- dev: false
- optional: true
-
- /@next/swc-linux-arm64-gnu@13.4.19:
- resolution: {integrity: sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [linux]
- requiresBuild: true
- dev: false
- optional: true
-
- /@next/swc-linux-arm64-musl@13.4.19:
- resolution: {integrity: sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [linux]
- requiresBuild: true
- dev: false
- optional: true
-
- /@next/swc-linux-x64-gnu@13.4.19:
- resolution: {integrity: sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [linux]
- requiresBuild: true
- dev: false
- optional: true
-
- /@next/swc-linux-x64-musl@13.4.19:
- resolution: {integrity: sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [linux]
- requiresBuild: true
- dev: false
- optional: true
-
- /@next/swc-win32-arm64-msvc@13.4.19:
- resolution: {integrity: sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==}
- engines: {node: '>= 10'}
- cpu: [arm64]
- os: [win32]
- requiresBuild: true
- dev: false
- optional: true
-
- /@next/swc-win32-ia32-msvc@13.4.19:
- resolution: {integrity: sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==}
- engines: {node: '>= 10'}
- cpu: [ia32]
- os: [win32]
- requiresBuild: true
- dev: false
- optional: true
-
- /@next/swc-win32-x64-msvc@13.4.19:
- resolution: {integrity: sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==}
- engines: {node: '>= 10'}
- cpu: [x64]
- os: [win32]
- requiresBuild: true
- dev: false
- optional: true
-
- /@storyblok/app-extension-auth@1.0.0-alpha.1:
- resolution: {integrity: sha512-qq5dGPmyGR9+nbRhYNTIN5vSgluGsozEdpXNwFVqVW8a1ZV/vq5aydqmDzua43f/EVcO+UA/CtIgvVnxt+xw8A==}
- engines: {node: '>=14.21.3'}
- dependencies:
- jsonwebtoken: 9.0.2
- openid-client: 5.6.1
- dev: false
-
- /@swc/helpers@0.5.1:
- resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==}
- dependencies:
- tslib: 2.6.2
- dev: false
-
- /@types/node@20.5.9:
- resolution: {integrity: sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==}
- dev: true
-
- /@types/prop-types@15.7.9:
- resolution: {integrity: sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==}
- dev: true
-
- /@types/react-dom@18.2.7:
- resolution: {integrity: sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==}
- dependencies:
- '@types/react': 18.2.21
- dev: true
-
- /@types/react@18.2.21:
- resolution: {integrity: sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==}
- dependencies:
- '@types/prop-types': 15.7.9
- '@types/scheduler': 0.16.5
- csstype: 3.1.2
- dev: true
-
- /@types/scheduler@0.16.5:
- resolution: {integrity: sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==}
- dev: true
-
- /buffer-equal-constant-time@1.0.1:
- resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
- dev: false
-
- /busboy@1.6.0:
- resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
- engines: {node: '>=10.16.0'}
- dependencies:
- streamsearch: 1.1.0
- dev: false
-
- /caniuse-lite@1.0.30001554:
- resolution: {integrity: sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ==}
- dev: false
-
- /client-only@0.0.1:
- resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
- dev: false
-
- /csstype@3.1.2:
- resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
- dev: true
-
- /ecdsa-sig-formatter@1.0.11:
- resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
- dependencies:
- safe-buffer: 5.2.1
- dev: false
-
- /glob-to-regexp@0.4.1:
- resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
- dev: false
-
- /graceful-fs@4.2.11:
- resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
- dev: false
-
- /jose@4.15.4:
- resolution: {integrity: sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==}
- dev: false
-
- /js-tokens@4.0.0:
- resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
- dev: false
-
- /jsonwebtoken@9.0.2:
- resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
- engines: {node: '>=12', npm: '>=6'}
- dependencies:
- jws: 3.2.2
- lodash.includes: 4.3.0
- lodash.isboolean: 3.0.3
- lodash.isinteger: 4.0.4
- lodash.isnumber: 3.0.3
- lodash.isplainobject: 4.0.6
- lodash.isstring: 4.0.1
- lodash.once: 4.1.1
- ms: 2.1.3
- semver: 7.5.4
- dev: false
-
- /jwa@1.4.1:
- resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
- dependencies:
- buffer-equal-constant-time: 1.0.1
- ecdsa-sig-formatter: 1.0.11
- safe-buffer: 5.2.1
- dev: false
-
- /jws@3.2.2:
- resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
- dependencies:
- jwa: 1.4.1
- safe-buffer: 5.2.1
- dev: false
-
- /lodash.includes@4.3.0:
- resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
- dev: false
-
- /lodash.isboolean@3.0.3:
- resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
- dev: false
-
- /lodash.isinteger@4.0.4:
- resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
- dev: false
-
- /lodash.isnumber@3.0.3:
- resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
- dev: false
-
- /lodash.isplainobject@4.0.6:
- resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
- dev: false
-
- /lodash.isstring@4.0.1:
- resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
- dev: false
-
- /lodash.once@4.1.1:
- resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
- dev: false
-
- /loose-envify@1.4.0:
- resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
- hasBin: true
- dependencies:
- js-tokens: 4.0.0
- dev: false
-
- /lru-cache@6.0.0:
- resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
- engines: {node: '>=10'}
- dependencies:
- yallist: 4.0.0
- dev: false
-
- /ms@2.1.3:
- resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
- dev: false
-
- /nanoid@3.3.6:
- resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
- engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
- hasBin: true
- dev: false
-
- /next@13.4.19(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==}
- engines: {node: '>=16.8.0'}
- hasBin: true
- peerDependencies:
- '@opentelemetry/api': ^1.1.0
- react: ^18.2.0
- react-dom: ^18.2.0
- sass: ^1.3.0
- peerDependenciesMeta:
- '@opentelemetry/api':
- optional: true
- sass:
- optional: true
- dependencies:
- '@next/env': 13.4.19
- '@swc/helpers': 0.5.1
- busboy: 1.6.0
- caniuse-lite: 1.0.30001554
- postcss: 8.4.14
- react: 18.2.0
- react-dom: 18.2.0(react@18.2.0)
- styled-jsx: 5.1.1(react@18.2.0)
- watchpack: 2.4.0
- zod: 3.21.4
- optionalDependencies:
- '@next/swc-darwin-arm64': 13.4.19
- '@next/swc-darwin-x64': 13.4.19
- '@next/swc-linux-arm64-gnu': 13.4.19
- '@next/swc-linux-arm64-musl': 13.4.19
- '@next/swc-linux-x64-gnu': 13.4.19
- '@next/swc-linux-x64-musl': 13.4.19
- '@next/swc-win32-arm64-msvc': 13.4.19
- '@next/swc-win32-ia32-msvc': 13.4.19
- '@next/swc-win32-x64-msvc': 13.4.19
- transitivePeerDependencies:
- - '@babel/core'
- - babel-plugin-macros
- dev: false
-
- /object-hash@2.2.0:
- resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
- engines: {node: '>= 6'}
- dev: false
-
- /oidc-token-hash@5.0.3:
- resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==}
- engines: {node: ^10.13.0 || >=12.0.0}
- dev: false
-
- /openid-client@5.6.1:
- resolution: {integrity: sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==}
- dependencies:
- jose: 4.15.4
- lru-cache: 6.0.0
- object-hash: 2.2.0
- oidc-token-hash: 5.0.3
- dev: false
-
- /picocolors@1.0.0:
- resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
- dev: false
-
- /postcss@8.4.14:
- resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==}
- engines: {node: ^10 || ^12 || >=14}
- dependencies:
- nanoid: 3.3.6
- picocolors: 1.0.0
- source-map-js: 1.0.2
- dev: false
-
- /react-dom@18.2.0(react@18.2.0):
- resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
- peerDependencies:
- react: ^18.2.0
- dependencies:
- loose-envify: 1.4.0
- react: 18.2.0
- scheduler: 0.23.0
- dev: false
-
- /react@18.2.0:
- resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
- engines: {node: '>=0.10.0'}
- dependencies:
- loose-envify: 1.4.0
- dev: false
-
- /safe-buffer@5.2.1:
- resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
- dev: false
-
- /scheduler@0.23.0:
- resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
- dependencies:
- loose-envify: 1.4.0
- dev: false
-
- /semver@7.5.4:
- resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
- engines: {node: '>=10'}
- hasBin: true
- dependencies:
- lru-cache: 6.0.0
- dev: false
-
- /source-map-js@1.0.2:
- resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
- engines: {node: '>=0.10.0'}
- dev: false
-
- /streamsearch@1.1.0:
- resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
- engines: {node: '>=10.0.0'}
- dev: false
-
- /styled-jsx@5.1.1(react@18.2.0):
- resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
- engines: {node: '>= 12.0.0'}
- peerDependencies:
- '@babel/core': '*'
- babel-plugin-macros: '*'
- react: '>= 16.8.0 || 17.x.x || ^18.0.0-0'
- peerDependenciesMeta:
- '@babel/core':
- optional: true
- babel-plugin-macros:
- optional: true
- dependencies:
- client-only: 0.0.1
- react: 18.2.0
- dev: false
-
- /tslib@2.6.2:
- resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
- dev: false
-
- /typescript@5.2.2:
- resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
- engines: {node: '>=14.17'}
- hasBin: true
- dev: false
-
- /watchpack@2.4.0:
- resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
- engines: {node: '>=10.13.0'}
- dependencies:
- glob-to-regexp: 0.4.1
- graceful-fs: 4.2.11
- dev: false
-
- /yallist@4.0.0:
- resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
- dev: false
-
- /zod@3.21.4:
- resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
- dev: false
diff --git a/tool-plugins/nextjs-starter/src/auth.ts b/tool-plugins/nextjs-starter/src/auth.ts
index ad29669..99ccf30 100644
--- a/tool-plugins/nextjs-starter/src/auth.ts
+++ b/tool-plugins/nextjs-starter/src/auth.ts
@@ -1,7 +1,7 @@
import {
authHandler,
AuthHandlerParams,
- sessionCookieStore,
+ getSessionStore,
} from '@storyblok/app-extension-auth';
['CLIENT_ID', 'CLIENT_SECRET', 'BASE_URL'].forEach((key) => {
if (!process.env[key]) {
@@ -9,17 +9,19 @@ import {
}
});
-export const cookieName = 'auth';
+export const sessionKey = 'auth';
export const authParams: AuthHandlerParams = {
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
baseUrl: process.env.BASE_URL!,
- cookieName,
+ sessionKey,
successCallback: '/',
errorCallback: '/401',
endpointPrefix: '/api/connect',
};
-export const appSessionCookies = sessionCookieStore(authParams);
+export const initOauthFlowUrl = `${authParams.endpointPrefix}/storyblok`;
+
+export const appSessionCookies = getSessionStore(authParams);
export const handleConnect = authHandler(authParams);
diff --git a/tool-plugins/nextjs-starter/src/components/Test.tsx b/tool-plugins/nextjs-starter/src/components/Test.tsx
new file mode 100644
index 0000000..93d0559
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/components/Test.tsx
@@ -0,0 +1,27 @@
+import { APP_BRIDGE_TOKEN_HEADER_KEY, KEY_TOKEN } from '@/utils/const';
+import { useEffect, useState } from 'react';
+
+export default function Test() {
+ const [testInfo, setTestInfo] = useState<{ verified: boolean }>({
+ verified: false,
+ });
+ useEffect(() => {
+ const fetchTestInfo = async () => {
+ const response = await fetch('/api/test', {
+ headers: {
+ [APP_BRIDGE_TOKEN_HEADER_KEY]:
+ sessionStorage.getItem(KEY_TOKEN) || '',
+ },
+ });
+ const json = await response.json();
+ setTestInfo(json);
+ };
+ fetchTestInfo();
+ }, []);
+
+ return (
+
+ App Bridge session is {testInfo?.verified ? 'verified' : 'not verified'}
+
+ );
+}
diff --git a/tool-plugins/nextjs-starter/src/components/UserInfo.tsx b/tool-plugins/nextjs-starter/src/components/UserInfo.tsx
new file mode 100644
index 0000000..960c05f
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/components/UserInfo.tsx
@@ -0,0 +1,15 @@
+import { useEffect, useState } from 'react';
+
+export default function UserInfo() {
+ const [userInfo, setUserInfo] = useState(null);
+ useEffect(() => {
+ const fetchUserInfo = async () => {
+ const response = await fetch('/api/user_info');
+ const json = await response.json();
+ setUserInfo(json);
+ };
+ fetchUserInfo();
+ }, []);
+
+ return {JSON.stringify(userInfo, null, 2)}
;
+}
diff --git a/tool-plugins/nextjs-starter/src/hooks/index.ts b/tool-plugins/nextjs-starter/src/hooks/index.ts
index d656c79..1a3df5a 100644
--- a/tool-plugins/nextjs-starter/src/hooks/index.ts
+++ b/tool-plugins/nextjs-starter/src/hooks/index.ts
@@ -1,3 +1,4 @@
export * from './useAutoHeight';
-export * from './getContext';
+export * from './useToolContext';
export * from './shared';
+export * from './useAppBridge';
diff --git a/tool-plugins/nextjs-starter/src/hooks/useAppBridge.ts b/tool-plugins/nextjs-starter/src/hooks/useAppBridge.ts
new file mode 100644
index 0000000..8658113
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/hooks/useAppBridge.ts
@@ -0,0 +1,277 @@
+import {
+ AppBridgeSession,
+ BeginOAuthMessagePayload,
+ CreateBeginOAuthMessagePayload,
+ CreateValidateMessagePayload,
+ PluginType,
+ PostMessageAction,
+ ValidateMessagePayload,
+} from '@/types';
+import {
+ APP_BRIDGE_ORIGIN,
+ KEY_PARENT_HOST,
+ KEY_SLUG,
+ KEY_TOKEN,
+ KEY_VALIDATED_PAYLOAD,
+} from '@/utils/const';
+import { useState, useEffect } from 'react';
+
+const getPostMessageAction = (type: PluginType): PostMessageAction => {
+ switch (type) {
+ case 'space-plugin':
+ return 'app-changed';
+ case 'tool-plugin':
+ return 'tool-changed';
+ default:
+ throw new Error(`Invalid plugin type: ${type}`);
+ }
+};
+
+const getParentHost = () => {
+ const storedHost = sessionStorage.getItem(KEY_PARENT_HOST);
+ if (storedHost) {
+ return storedHost;
+ }
+ const params = new URLSearchParams(location.search);
+ const protocol = params.get('protocol');
+ const host = params.get('host');
+ if (!protocol || !host) {
+ throw new Error('Missing `protocol` or `host` in query params');
+ }
+ return `${protocol}//${host}`;
+};
+
+const getSlug = () => {
+ const storedSlug = sessionStorage.getItem(KEY_SLUG);
+ if (storedSlug) {
+ return storedSlug;
+ }
+ const params = new URLSearchParams(location.search);
+ return params.get('slug');
+};
+
+const postMessageToParent = (payload: unknown) => {
+ window.parent.postMessage(payload, getParentHost());
+};
+
+const useAppBridgeAuth = ({
+ type,
+ authenticated,
+}: {
+ type: PluginType;
+ authenticated: () => Promise;
+}) => {
+ const [status, setStatus] = useState<
+ 'init' | 'authenticating' | 'authenticated' | 'error'
+ >('init');
+ const [error, setError] = useState();
+
+ const init = async () => {
+ const isInIframe = window.top !== window.self;
+
+ if (!isInIframe) {
+ setStatus('error');
+ setError('not-in-iframe');
+ return;
+ }
+
+ if (!isAuthenticated()) {
+ sendValidateMessageToParent();
+ return;
+ }
+
+ setStatus('authenticated');
+ setError(undefined);
+
+ await authenticated();
+ };
+
+ const isAuthenticated = () => {
+ try {
+ const payload: AppBridgeSession = JSON.parse(
+ sessionStorage.getItem(KEY_VALIDATED_PAYLOAD) || '',
+ );
+ return payload && new Date().getTime() / 1000 < payload.exp;
+ } catch (err) {
+ return false;
+ }
+ };
+
+ const sendValidateMessageToParent = () => {
+ setStatus('authenticating');
+ setError(undefined);
+ const host = getParentHost();
+ const slug = getSlug();
+
+ try {
+ const payload = createValidateMessagePayload({ type, slug });
+
+ postMessageToParent(payload);
+ sessionStorage.setItem(KEY_PARENT_HOST, host);
+ sessionStorage.setItem(KEY_SLUG, slug || '');
+ } catch (err) {
+ sessionStorage.removeItem(KEY_PARENT_HOST);
+ }
+ };
+
+ const createValidateMessagePayload: CreateValidateMessagePayload = ({
+ type,
+ slug,
+ }) => {
+ const payload: ValidateMessagePayload = {
+ action: getPostMessageAction(type),
+ event: 'validate',
+ };
+
+ if (type === 'tool-plugin') {
+ payload.tool = slug;
+ }
+
+ return payload;
+ };
+
+ const eventListener = async (event: MessageEvent) => {
+ if (event.origin !== APP_BRIDGE_ORIGIN) {
+ // This can happen for many different reasons,
+ // like a React DevTools extension, etc.
+ return;
+ }
+
+ if (event.data.action === 'validated') {
+ const token = event.data.token;
+ try {
+ const response = await (
+ await fetch('/api/_app_bridge', {
+ method: 'POST',
+ body: JSON.stringify({ token }),
+ })
+ ).json();
+
+ if (response.ok) {
+ sessionStorage.setItem(KEY_TOKEN, token);
+ sessionStorage.setItem(
+ KEY_VALIDATED_PAYLOAD,
+ JSON.stringify(response.result),
+ );
+ setStatus('authenticated');
+ setError(undefined);
+ await authenticated();
+ } else {
+ sessionStorage.removeItem(KEY_TOKEN);
+ sessionStorage.removeItem(KEY_VALIDATED_PAYLOAD);
+ setStatus('error');
+ setError(response.error);
+ }
+ } catch (err) {
+ sessionStorage.removeItem(KEY_TOKEN);
+ sessionStorage.removeItem(KEY_VALIDATED_PAYLOAD);
+ setStatus('error');
+ setError(err);
+ }
+ }
+ };
+
+ useEffect(() => {
+ // Adds event listener to listen to events coming from Storyblok to Iframe (plugin)
+ window.addEventListener('message', eventListener);
+
+ return () => {
+ window.removeEventListener('message', eventListener);
+ };
+ }, []);
+
+ return { status, init, error };
+};
+
+const useOAuth = ({ type }: { type: PluginType }) => {
+ const [status, setStatus] = useState<
+ 'init' | 'authenticating' | 'authenticated'
+ >('init');
+
+ const init = async () => {
+ setStatus('authenticating');
+
+ const initOAuth =
+ new URLSearchParams(location.search).get('init_oauth') === 'true';
+
+ const response = await (
+ await fetch('/api/_oauth', {
+ method: 'POST',
+ body: JSON.stringify({ initOAuth }),
+ })
+ ).json();
+
+ if (response.ok) {
+ setStatus('authenticated');
+ return;
+ }
+
+ if (initOAuth) {
+ sendBeginOAuthMessageToParent(response.redirectTo);
+ } else {
+ window.location.href = response.redirectTo;
+ }
+ };
+
+ const sendBeginOAuthMessageToParent = (redirectTo: string) => {
+ const slug = getSlug();
+ const payload = createOAuthInitMessagePayload({ type, slug, redirectTo });
+ postMessageToParent(payload);
+ };
+
+ const createOAuthInitMessagePayload: CreateBeginOAuthMessagePayload = ({
+ type,
+ slug,
+ redirectTo,
+ }) => {
+ const payload: BeginOAuthMessagePayload = {
+ action: getPostMessageAction(type),
+ event: 'beginOAuth',
+ redirectTo,
+ };
+
+ if (type === 'tool-plugin') {
+ payload.tool = slug;
+ }
+
+ return payload;
+ };
+
+ return { init, status };
+};
+
+export const useAppBridge = ({
+ type,
+ oauth,
+}: {
+ type: PluginType;
+ oauth: boolean;
+}) => {
+ const { init: initOAuth, status: oauthStatus } = useOAuth({ type });
+
+ const { init: initAppBridgeAuth, status: appBridgeAuthStatus } =
+ useAppBridgeAuth({
+ type,
+ authenticated: async () => {
+ if (oauth) {
+ await initOAuth();
+ }
+ },
+ });
+
+ const completed = oauth
+ ? appBridgeAuthStatus === 'authenticated' && oauthStatus === 'authenticated'
+ : appBridgeAuthStatus === 'authenticated';
+
+ useEffect(() => {
+ initAppBridgeAuth();
+ }, [type, oauth]);
+
+ return {
+ completed,
+ appBridgeAuth: appBridgeAuthStatus,
+ oauth: oauthStatus,
+ getSlug,
+ getParentHost,
+ };
+};
diff --git a/tool-plugins/nextjs-starter/src/hooks/getContext.ts b/tool-plugins/nextjs-starter/src/hooks/useToolContext.ts
similarity index 100%
rename from tool-plugins/nextjs-starter/src/hooks/getContext.ts
rename to tool-plugins/nextjs-starter/src/hooks/useToolContext.ts
diff --git a/tool-plugins/nextjs-starter/src/pages/401.tsx b/tool-plugins/nextjs-starter/src/pages/401.tsx
index 6a38d27..a6c6db4 100644
--- a/tool-plugins/nextjs-starter/src/pages/401.tsx
+++ b/tool-plugins/nextjs-starter/src/pages/401.tsx
@@ -9,5 +9,5 @@ export default function Error401() {
}
}, []);
- return Error: Unauthorized;
+ return ;
}
diff --git a/tool-plugins/nextjs-starter/src/pages/api/_app_bridge.ts b/tool-plugins/nextjs-starter/src/pages/api/_app_bridge.ts
new file mode 100644
index 0000000..ea9eac8
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/pages/api/_app_bridge.ts
@@ -0,0 +1,15 @@
+import { verifyAppBridgeToken } from '@/utils/server';
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse,
+) {
+ if (req.method !== 'POST') {
+ return res.status(405).json({ ok: false, error: 'Method Not Allowed' });
+ }
+
+ const { token } = JSON.parse(req.body);
+ const result = await verifyAppBridgeToken(token);
+ return res.status(200).json(result);
+}
diff --git a/tool-plugins/nextjs-starter/src/pages/api/_oauth.ts b/tool-plugins/nextjs-starter/src/pages/api/_oauth.ts
new file mode 100644
index 0000000..674657d
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/pages/api/_oauth.ts
@@ -0,0 +1,28 @@
+import { initOauthFlowUrl } from '@/auth';
+import type { NextApiRequest, NextApiResponse } from 'next';
+import { getAppSession } from '@/utils/server';
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse,
+) {
+ const { initOAuth } = JSON.parse(req.body);
+ if (initOAuth) {
+ return res.status(200).json({
+ ok: false,
+ redirectTo: initOauthFlowUrl,
+ });
+ }
+
+ const appSession = await getAppSession(req, res);
+ if (appSession) {
+ return res.status(200).json({
+ ok: true,
+ });
+ }
+
+ return res.status(200).json({
+ ok: false,
+ redirectTo: initOauthFlowUrl,
+ });
+}
diff --git a/tool-plugins/nextjs-starter/src/pages/api/test.ts b/tool-plugins/nextjs-starter/src/pages/api/test.ts
new file mode 100644
index 0000000..eed1304
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/pages/api/test.ts
@@ -0,0 +1,26 @@
+import { verifyAppBridgeHeader } from '@/utils/server';
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse,
+) {
+ const verified = await verifyAppBridgeHeader(req);
+
+ if (verified.ok) {
+ // perform something with verified app bridge session
+ /*
+ verified.result = {
+ app_id: number;
+ space_id: number;
+ user_id: number;
+ iat: number;
+ exp: number;
+ }
+ */
+ }
+
+ return res.status(200).json({
+ verified: verified.ok,
+ });
+}
diff --git a/tool-plugins/nextjs-starter/src/pages/api/user_info.ts b/tool-plugins/nextjs-starter/src/pages/api/user_info.ts
new file mode 100644
index 0000000..81fed1c
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/pages/api/user_info.ts
@@ -0,0 +1,33 @@
+import { getAppSession } from '@/utils/server';
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse,
+) {
+ const appSession = await getAppSession(req, res);
+ if (!appSession) {
+ return res.status(401).end();
+ }
+ return res.status(200).json(await fetchUserInfo(appSession.accessToken));
+}
+
+const fetchUserInfo = async (accessToken: string) => {
+ try {
+ const response = await fetch(`https://api.storyblok.com/oauth/user_info`, {
+ headers: {
+ Authorization: `Bearer ${accessToken}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Request failed with status: ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ console.error('Failed to fetch user information:', error);
+ }
+
+ return null;
+};
diff --git a/tool-plugins/nextjs-starter/src/pages/index.tsx b/tool-plugins/nextjs-starter/src/pages/index.tsx
index 00b196a..eb33342 100644
--- a/tool-plugins/nextjs-starter/src/pages/index.tsx
+++ b/tool-plugins/nextjs-starter/src/pages/index.tsx
@@ -1,9 +1,7 @@
import Head from 'next/head';
-import { GetServerSideProps } from 'next';
-import { isAppSessionQuery } from '@storyblok/app-extension-auth';
-import { appSessionCookies } from '@/auth';
-import { useAutoHeight, useToolContext } from '@/hooks';
-import { isAdmin } from '@/utils';
+import { useAppBridge, useToolContext } from '@/hooks';
+import UserInfo from '@/components/UserInfo';
+import Test from '@/components/Test';
type User = {
id: number;
@@ -14,17 +12,9 @@ type UserInfo = {
user: User;
};
-type HomeProps = {
- userInfo: UserInfo;
- spaceId: string;
- userId: string;
- isAdmin: boolean;
-};
-
-export default function Home(props: HomeProps) {
+export default function Home() {
const toolContext = useToolContext();
-
- useAutoHeight();
+ const { completed } = useAppBridge({ type: 'tool-plugin', oauth: true });
return (
<>
@@ -34,72 +24,21 @@ export default function Home(props: HomeProps) {
- {props.userInfo && (
- Hello {props.userInfo.user.friendly_name}
- )}
- {toolContext && (
- <>
- Story Information
-
- Story: {toolContext.story.name}
- Slug: {toolContext.story.slug}
-
- >
+ {completed && (
+
+
Authenticated!
+
+
+ {toolContext && (
+
+
Tool Context
+
Story: {toolContext.story.name}
+
Slug: {toolContext.story.slug}
+
+ )}
+
)}
>
);
}
-
-export const getServerSideProps: GetServerSideProps = async (context) => {
- const { query } = context;
-
- if (!isAppSessionQuery(query)) {
- return {
- redirect: {
- permanent: false,
- destination: process.env.BASE_URL + '/api/connect/storyblok',
- },
- };
- }
-
- const sessionStore = appSessionCookies(context);
- const appSession = await sessionStore.get(query);
-
- if (!appSession) {
- return {
- redirect: {
- permanent: false,
- destination: process.env.BASE_URL + '/api/connect/storyblok',
- },
- };
- }
-
- const { accessToken, spaceId, userId } = appSession;
-
- const userInfo = await fetchUserInfo(accessToken);
-
- return {
- props: { userInfo, spaceId, userId, isAdmin: isAdmin(appSession) },
- };
-};
-
-const fetchUserInfo = async (accessToken: string) => {
- try {
- const response = await fetch(`https://api.storyblok.com/oauth/user_info`, {
- headers: {
- Authorization: `Bearer ${accessToken}`,
- },
- });
-
- if (!response.ok) {
- throw new Error(`Request failed with status: ${response.status}`);
- }
-
- return await response.json();
- } catch (error) {
- console.error('Failed to fetch user information:', error);
- }
-
- return null;
-};
diff --git a/tool-plugins/nextjs-starter/src/types/appBridge.ts b/tool-plugins/nextjs-starter/src/types/appBridge.ts
new file mode 100644
index 0000000..4d8ba2c
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/types/appBridge.ts
@@ -0,0 +1,53 @@
+export type AppBridgeConfig = {
+ enabled: boolean;
+ oauth: boolean;
+ origin?: string;
+};
+
+export type VerifyResponse =
+ | { ok: true; result: AppBridgeSession }
+ | { ok: false; error: unknown };
+
+export type AppBridgeSession = {
+ app_id: number;
+ space_id: number;
+ user_id: number;
+ iat: number;
+ exp: number;
+};
+
+export type PluginType = 'space-plugin' | 'tool-plugin';
+
+export type UseAppBridgeParams = {
+ type: PluginType;
+};
+
+export type UseAppBridgeMessagesParams = {
+ type: PluginType;
+};
+
+export type PostMessageAction = 'tool-changed' | 'app-changed';
+
+export type ValidateMessagePayload = {
+ action: PostMessageAction;
+ event: 'validate';
+ tool?: string | null;
+};
+
+export type BeginOAuthMessagePayload = {
+ action: PostMessageAction;
+ event: 'beginOAuth';
+ tool?: string | null;
+ redirectTo: string;
+};
+
+export type CreateValidateMessagePayload = (params: {
+ type: PluginType;
+ slug: string | null;
+}) => ValidateMessagePayload;
+
+export type CreateBeginOAuthMessagePayload = (params: {
+ type: PluginType;
+ slug: string | null;
+ redirectTo: string;
+}) => BeginOAuthMessagePayload;
diff --git a/tool-plugins/nextjs-starter/src/types/index.ts b/tool-plugins/nextjs-starter/src/types/index.ts
new file mode 100644
index 0000000..cdcb605
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/types/index.ts
@@ -0,0 +1 @@
+export * from './appBridge';
diff --git a/tool-plugins/nextjs-starter/src/utils/const.ts b/tool-plugins/nextjs-starter/src/utils/const.ts
new file mode 100644
index 0000000..3b4503d
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/utils/const.ts
@@ -0,0 +1,9 @@
+export const KEY_PREFIX = 'sb_ab';
+export const KEY_TOKEN = `${KEY_PREFIX}_token`;
+export const KEY_VALIDATED_PAYLOAD = `${KEY_PREFIX}_validated_payload`;
+export const KEY_PARENT_HOST = `${KEY_PREFIX}_parent_host`;
+export const KEY_SLUG = `${KEY_PREFIX}_slug`;
+
+export const APP_BRIDGE_TOKEN_HEADER_KEY = 'sb_app_bridge_token';
+
+export const APP_BRIDGE_ORIGIN = 'https://app.storyblok.com';
diff --git a/tool-plugins/nextjs-starter/src/utils/server/appBridge.ts b/tool-plugins/nextjs-starter/src/utils/server/appBridge.ts
new file mode 100644
index 0000000..fac20c9
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/utils/server/appBridge.ts
@@ -0,0 +1,34 @@
+import jwt, { type VerifyCallback } from 'jsonwebtoken';
+import { AppBridgeSession, VerifyResponse } from '@/types';
+import { NextApiRequest } from 'next';
+import { APP_BRIDGE_TOKEN_HEADER_KEY } from '../const';
+
+export const verifyAppBridgeHeader = async (req: NextApiRequest) => {
+ const token = req.headers[APP_BRIDGE_TOKEN_HEADER_KEY];
+ const result = await verifyAppBridgeToken(token as string);
+ return result;
+};
+
+export const verifyAppBridgeToken = async (
+ token: string,
+): Promise => {
+ try {
+ return {
+ ok: true,
+ result: await verifyToken(token, process.env.CLIENT_SECRET || ''),
+ };
+ } catch (error) {
+ return { ok: false, error };
+ }
+};
+
+async function verifyToken(
+ token: string,
+ secret: string,
+): Promise {
+ return new Promise((resolve, reject) => {
+ const verifyCallback: VerifyCallback = (err, decoded) =>
+ err ? reject(err) : resolve(decoded as AppBridgeSession);
+ jwt.verify(token, secret, verifyCallback);
+ });
+}
diff --git a/tool-plugins/nextjs-starter/src/utils/server/index.ts b/tool-plugins/nextjs-starter/src/utils/server/index.ts
new file mode 100644
index 0000000..0461668
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/utils/server/index.ts
@@ -0,0 +1,2 @@
+export * from './appBridge';
+export * from './oauth';
diff --git a/tool-plugins/nextjs-starter/src/utils/server/oauth.ts b/tool-plugins/nextjs-starter/src/utils/server/oauth.ts
new file mode 100644
index 0000000..7481d58
--- /dev/null
+++ b/tool-plugins/nextjs-starter/src/utils/server/oauth.ts
@@ -0,0 +1,22 @@
+import { authParams } from '@/auth';
+import type { NextApiRequest, NextApiResponse } from 'next';
+import {
+ getSessionStore,
+ inferSessionQuery,
+} from '@storyblok/app-extension-auth';
+
+export const getAppSession = async (
+ req: NextApiRequest,
+ res: NextApiResponse,
+) => {
+ const sessionStore = getSessionStore(authParams)({
+ req,
+ res,
+ });
+
+ const appSessionQuery = inferSessionQuery(req);
+ if (!appSessionQuery) {
+ return;
+ }
+ return await sessionStore.get(appSessionQuery);
+};
diff --git a/tool-plugins/nextjs-starter/yarn.lock b/tool-plugins/nextjs-starter/yarn.lock
index 315c800..0387de8 100644
--- a/tool-plugins/nextjs-starter/yarn.lock
+++ b/tool-plugins/nextjs-starter/yarn.lock
@@ -75,14 +75,14 @@ __metadata:
languageName: node
linkType: hard
-"@storyblok/app-extension-auth@npm:1.0.3":
- version: 1.0.3
- resolution: "@storyblok/app-extension-auth@npm:1.0.3"
+"@storyblok/app-extension-auth@npm:2.0.0":
+ version: 2.0.0
+ resolution: "@storyblok/app-extension-auth@npm:2.0.0"
dependencies:
"@storyblok/region-helper": 0.1.0
jsonwebtoken: ^9.0.0
openid-client: ^5.4.2
- checksum: 53642504f45ebc30512f5af372b425b0d48b0713ab801500262827ee707d0582059c645958f87ba3d593a3742c2f499b087705c902e03fc761ed8901d6716b92
+ checksum: 839bc19914e1a790e8d24db6d95e614aad0b21ef0919819a4e92f33f5be0fa7d21c51e4108ec47c94352d541ca4c3f48d5d615f33b617b44721f479116d82ad9
languageName: node
linkType: hard
@@ -102,6 +102,24 @@ __metadata:
languageName: node
linkType: hard
+"@types/jsonwebtoken@npm:^9.0.7":
+ version: 9.0.7
+ resolution: "@types/jsonwebtoken@npm:9.0.7"
+ dependencies:
+ "@types/node": "*"
+ checksum: 872b62e2a50ec399d695402ccddfeb5cd66a6c3d28511f27453b932b6b67eb82c2d0ecaa864939848b88b3a8276c2492647bf5707bc82a6ac7e420d3412b9047
+ languageName: node
+ linkType: hard
+
+"@types/node@npm:*":
+ version: 22.5.5
+ resolution: "@types/node@npm:22.5.5"
+ dependencies:
+ undici-types: ~6.19.2
+ checksum: 1f788966ff7df07add0af3481fb68c7fe5091cc72a265c671432abb443788ddacca4ca6378af64fe100c20f857c4d80170d358e66c070171fcea0d4adb1b45b1
+ languageName: node
+ linkType: hard
+
"@types/node@npm:20.5.9":
version: 20.5.9
resolution: "@types/node@npm:20.5.9"
@@ -110,9 +128,9 @@ __metadata:
linkType: hard
"@types/prop-types@npm:*":
- version: 15.7.5
- resolution: "@types/prop-types@npm:15.7.5"
- checksum: 5b43b8b15415e1f298243165f1d44390403bb2bd42e662bca3b5b5633fdd39c938e91b7fce3a9483699db0f7a715d08cef220c121f723a634972fdf596aec980
+ version: 15.7.13
+ resolution: "@types/prop-types@npm:15.7.13"
+ checksum: 8935cad87c683c665d09a055919d617fe951cb3b2d5c00544e3a913f861a2bd8d2145b51c9aa6d2457d19f3107ab40784c40205e757232f6a80cc8b1c815513c
languageName: node
linkType: hard
@@ -125,7 +143,17 @@ __metadata:
languageName: node
linkType: hard
-"@types/react@npm:*, @types/react@npm:18.2.21":
+"@types/react@npm:*":
+ version: 18.3.7
+ resolution: "@types/react@npm:18.3.7"
+ dependencies:
+ "@types/prop-types": "*"
+ csstype: ^3.0.2
+ checksum: 027cf84d8309c4d0a9b16ec26f71de0950e2d748293bbc4dac42519f77d0bec099aeb5fb1c0bcb891725973e53085c1aedea5c3a16bca215c2fc2ecf68c7ec6e
+ languageName: node
+ linkType: hard
+
+"@types/react@npm:18.2.21":
version: 18.2.21
resolution: "@types/react@npm:18.2.21"
dependencies:
@@ -137,9 +165,9 @@ __metadata:
linkType: hard
"@types/scheduler@npm:*":
- version: 0.16.3
- resolution: "@types/scheduler@npm:0.16.3"
- checksum: 2b0aec39c24268e3ce938c5db2f2e77f5c3dd280e05c262d9c2fe7d890929e4632a6b8e94334017b66b45e4f92a5aa42ba3356640c2a1175fa37bef2f5200767
+ version: 0.23.0
+ resolution: "@types/scheduler@npm:0.23.0"
+ checksum: 874d753aa65c17760dfc460a91e6df24009bde37bfd427a031577b30262f7770c1b8f71a21366c7dbc76111967384cf4090a31d65315155180ef14bd7acccb32
languageName: node
linkType: hard
@@ -160,9 +188,9 @@ __metadata:
linkType: hard
"caniuse-lite@npm:^1.0.30001406":
- version: 1.0.30001528
- resolution: "caniuse-lite@npm:1.0.30001528"
- checksum: 7b6a71d41de1ce2b95f5851e7074050b7600157acf02c5701d9650a1faf824e368163f4ede5e269d4ea44fe33e00fe60673cb86f40cac35bd02a9a9a4ef7f162
+ version: 1.0.30001662
+ resolution: "caniuse-lite@npm:1.0.30001662"
+ checksum: 7a6a0c0d9f7c4a1c51de02838eb47f41f36fff57a77b846c8faed35ba9afba17b9399bc00bd637e5c1663cbc132534085d91151de48edca2ad8932a5d87e23af
languageName: node
linkType: hard
@@ -174,9 +202,9 @@ __metadata:
linkType: hard
"csstype@npm:^3.0.2":
- version: 3.1.2
- resolution: "csstype@npm:3.1.2"
- checksum: e1a52e6c25c1314d6beef5168da704ab29c5186b877c07d822bd0806717d9a265e8493a2e35ca7e68d0f5d472d43fac1cdce70fd79fd0853dff81f3028d857b5
+ version: 3.1.3
+ resolution: "csstype@npm:3.1.3"
+ checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7
languageName: node
linkType: hard
@@ -203,10 +231,10 @@ __metadata:
languageName: node
linkType: hard
-"jose@npm:^4.14.4":
- version: 4.14.6
- resolution: "jose@npm:4.14.6"
- checksum: eae81a234e7bf1446b1bd80722b3462b014e3835b155c3a7799c1c5043163a53a0dc28d347004151b031e6b7b863403aabf8814d9cc217ce21f8c2f3ebd4b335
+"jose@npm:^4.15.9":
+ version: 4.15.9
+ resolution: "jose@npm:4.15.9"
+ checksum: 41abe1c99baa3cf8a78ebbf93da8f8e50e417b7a26754c4afa21865d87527b8ac2baf66de2c5f6accc3f7d7158658dae7364043677236ea1d07895b040097f15
languageName: node
linkType: hard
@@ -217,7 +245,7 @@ __metadata:
languageName: node
linkType: hard
-"jsonwebtoken@npm:^9.0.0":
+"jsonwebtoken@npm:^9.0.0, jsonwebtoken@npm:^9.0.2":
version: 9.0.2
resolution: "jsonwebtoken@npm:9.0.2"
dependencies:
@@ -333,11 +361,11 @@ __metadata:
linkType: hard
"nanoid@npm:^3.3.4":
- version: 3.3.6
- resolution: "nanoid@npm:3.3.6"
+ version: 3.3.7
+ resolution: "nanoid@npm:3.3.7"
bin:
nanoid: bin/nanoid.cjs
- checksum: 7d0eda657002738aa5206107bd0580aead6c95c460ef1bdd0b1a87a9c7ae6277ac2e9b945306aaa5b32c6dcb7feaf462d0f552e7f8b5718abfc6ead5c94a71b3
+ checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2
languageName: node
linkType: hard
@@ -412,21 +440,21 @@ __metadata:
linkType: hard
"openid-client@npm:^5.4.2":
- version: 5.4.3
- resolution: "openid-client@npm:5.4.3"
+ version: 5.7.0
+ resolution: "openid-client@npm:5.7.0"
dependencies:
- jose: ^4.14.4
+ jose: ^4.15.9
lru-cache: ^6.0.0
object-hash: ^2.2.0
oidc-token-hash: ^5.0.3
- checksum: 0e5a126b77dad0320e8f7023ac7ad7f5f1f82ad5f985f7ab0b42a7cf36700dfb78f0bef9b59c1fae915dce0148ef191b49921cd0a01443b64c04f862d9dc03e0
+ checksum: 63fc76918fc12f3d6e1456a0b170f417defccf6820acb4581ffc226cb8c9a18d50f76f0982d7a00cce2896c732eb2a6361ad6ea04b127b2603e56408b680ef9c
languageName: node
linkType: hard
"picocolors@npm:^1.0.0":
- version: 1.0.0
- resolution: "picocolors@npm:1.0.0"
- checksum: a2e8092dd86c8396bdba9f2b5481032848525b3dc295ce9b57896f931e63fc16f79805144321f72976383fc249584672a75cc18d6777c6b757603f372f745981
+ version: 1.1.0
+ resolution: "picocolors@npm:1.1.0"
+ checksum: a64d653d3a188119ff45781dfcdaeedd7625583f45280aea33fcb032c7a0d3959f2368f9b192ad5e8aade75b74dbd954ffe3106c158509a45e4c18ab379a2acd
languageName: node
linkType: hard
@@ -470,29 +498,27 @@ __metadata:
linkType: hard
"scheduler@npm:^0.23.0":
- version: 0.23.0
- resolution: "scheduler@npm:0.23.0"
+ version: 0.23.2
+ resolution: "scheduler@npm:0.23.2"
dependencies:
loose-envify: ^1.1.0
- checksum: d79192eeaa12abef860c195ea45d37cbf2bbf5f66e3c4dcd16f54a7da53b17788a70d109ee3d3dde1a0fd50e6a8fc171f4300356c5aee4fc0171de526bf35f8a
+ checksum: 3e82d1f419e240ef6219d794ff29c7ee415fbdc19e038f680a10c067108e06284f1847450a210b29bbaf97b9d8a97ced5f624c31c681248ac84c80d56ad5a2c4
languageName: node
linkType: hard
"semver@npm:^7.5.4":
- version: 7.5.4
- resolution: "semver@npm:7.5.4"
- dependencies:
- lru-cache: ^6.0.0
+ version: 7.6.3
+ resolution: "semver@npm:7.6.3"
bin:
semver: bin/semver.js
- checksum: 12d8ad952fa353b0995bf180cdac205a4068b759a140e5d3c608317098b3575ac2f1e09182206bf2eb26120e1c0ed8fb92c48c592f6099680de56bb071423ca3
+ checksum: 4110ec5d015c9438f322257b1c51fe30276e5f766a3f64c09edd1d7ea7118ecbc3f379f3b69032bacf13116dc7abc4ad8ce0d7e2bd642e26b0d271b56b61a7d8
languageName: node
linkType: hard
"source-map-js@npm:^1.0.2":
- version: 1.0.2
- resolution: "source-map-js@npm:1.0.2"
- checksum: c049a7fc4deb9a7e9b481ae3d424cc793cb4845daa690bc5a05d428bf41bf231ced49b4cf0c9e77f9d42fdb3d20d6187619fc586605f5eabe995a316da8d377c
+ version: 1.2.1
+ resolution: "source-map-js@npm:1.2.1"
+ checksum: 4eb0cd997cdf228bc253bcaff9340afeb706176e64868ecd20efbe6efea931465f43955612346d6b7318789e5265bdc419bc7669c1cebe3db0eb255f57efa76b
languageName: node
linkType: hard
@@ -523,10 +549,12 @@ __metadata:
version: 0.0.0-use.local
resolution: "tool-nextjs-starter@workspace:."
dependencies:
- "@storyblok/app-extension-auth": 1.0.3
+ "@storyblok/app-extension-auth": 2.0.0
+ "@types/jsonwebtoken": ^9.0.7
"@types/node": 20.5.9
"@types/react": 18.2.21
"@types/react-dom": 18.2.7
+ jsonwebtoken: ^9.0.2
next: 13.4.19
react: 18.2.0
react-dom: 18.2.0
@@ -535,9 +563,9 @@ __metadata:
linkType: soft
"tslib@npm:^2.4.0":
- version: 2.6.2
- resolution: "tslib@npm:2.6.2"
- checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad
+ version: 2.7.0
+ resolution: "tslib@npm:2.7.0"
+ checksum: 1606d5c89f88d466889def78653f3aab0f88692e80bb2066d090ca6112ae250ec1cfa9dbfaab0d17b60da15a4186e8ec4d893801c67896b277c17374e36e1d28
languageName: node
linkType: hard
@@ -561,6 +589,13 @@ __metadata:
languageName: node
linkType: hard
+"undici-types@npm:~6.19.2":
+ version: 6.19.8
+ resolution: "undici-types@npm:6.19.8"
+ checksum: de51f1b447d22571cf155dfe14ff6d12c5bdaec237c765085b439c38ca8518fc360e88c70f99469162bf2e14188a7b0bcb06e1ed2dc031042b984b0bb9544017
+ languageName: node
+ linkType: hard
+
"watchpack@npm:2.4.0":
version: 2.4.0
resolution: "watchpack@npm:2.4.0"