sanity-plugin-preview-auth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +142 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
- package/src/actions/openPreviewAction.ts +70 -0
- package/src/components/PreviewAuthPage.tsx +91 -0
- package/src/hooks/usePreviewAuth.ts +102 -0
- package/src/index.ts +7 -0
- package/src/plugin.ts +46 -0
- package/src/schema/previewAuthSecret.ts +21 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: !0 });
|
|
3
|
+
var sanity = require("sanity"), icons = require("@sanity/icons"), jsxRuntime = require("react/jsx-runtime"), ui = require("@sanity/ui"), react = require("react"), sanityPluginPreviewAuthValidate = require("sanity-plugin-preview-auth-validate");
|
|
4
|
+
function usePreviewAuth({
|
|
5
|
+
previewOrigin,
|
|
6
|
+
previewAuthApi
|
|
7
|
+
}) {
|
|
8
|
+
const client = sanity.useClient(sanity.DEFAULT_STUDIO_CLIENT_OPTIONS), currentUser = sanity.useCurrentUser(), [isAuthenticating, setIsAuthenticating] = react.useState(!1), [error, setError] = react.useState(null), redirectUrl = react.useMemo(() => typeof window > "u" ? null : new URLSearchParams(window.location.search).get("redirect"), []), fullRedirectUrl = react.useMemo(() => {
|
|
9
|
+
if (!redirectUrl || !previewOrigin)
|
|
10
|
+
return null;
|
|
11
|
+
try {
|
|
12
|
+
return new URL(redirectUrl, previewOrigin).toString();
|
|
13
|
+
} catch {
|
|
14
|
+
return `${previewOrigin}${redirectUrl}`;
|
|
15
|
+
}
|
|
16
|
+
}, [redirectUrl, previewOrigin]), handleAuthenticate = react.useCallback(async () => {
|
|
17
|
+
if (!previewOrigin) {
|
|
18
|
+
setError("No preview URL configured for this workspace.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
setIsAuthenticating(!0), setError(null);
|
|
22
|
+
try {
|
|
23
|
+
const { secret } = await sanityPluginPreviewAuthValidate.createPreviewSecret({
|
|
24
|
+
client,
|
|
25
|
+
source: "sanity/preview-auth",
|
|
26
|
+
studioUrl: window.location.href,
|
|
27
|
+
userId: currentUser?.id
|
|
28
|
+
}), apiUrl = new URL(previewAuthApi, previewOrigin), redirectPath = redirectUrl?.startsWith("/preview") ? redirectUrl.replace(/^\/preview/, "") || "/" : redirectUrl || "/";
|
|
29
|
+
apiUrl.searchParams.set("sanity-preview-secret", secret), apiUrl.searchParams.set("sanity-preview-pathname", redirectPath), apiUrl.searchParams.set("response", "json");
|
|
30
|
+
const response = await fetch(apiUrl, {
|
|
31
|
+
method: "GET",
|
|
32
|
+
credentials: "include",
|
|
33
|
+
headers: { Accept: "application/json" }
|
|
34
|
+
}), payload = await response.json().catch(() => null);
|
|
35
|
+
if (!response.ok || payload?.ok === !1)
|
|
36
|
+
throw new Error(payload?.error || `Authentication failed (${response.status})`);
|
|
37
|
+
if (!payload?.redirectTo)
|
|
38
|
+
throw new Error("Missing redirect target from authentication response.");
|
|
39
|
+
window.location.href = new URL(payload.redirectTo, previewOrigin).toString();
|
|
40
|
+
} catch (err) {
|
|
41
|
+
setError(err instanceof Error ? err.message : "Failed to authenticate preview mode."), setIsAuthenticating(!1);
|
|
42
|
+
}
|
|
43
|
+
}, [client, currentUser?.id, previewAuthApi, previewOrigin, redirectUrl]);
|
|
44
|
+
return { redirectUrl, fullRedirectUrl, isAuthenticating, error, handleAuthenticate };
|
|
45
|
+
}
|
|
46
|
+
function PreviewAuthPage({ previewOrigin, previewAuthApi }) {
|
|
47
|
+
const { redirectUrl, fullRedirectUrl, isAuthenticating, error, handleAuthenticate } = usePreviewAuth({ previewOrigin, previewAuthApi });
|
|
48
|
+
return redirectUrl ? /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { height: "fill", overflow: "auto", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { width: 1, padding: 5, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "center", align: "center", style: { minHeight: "60vh" }, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 5, radius: 3, shadow: 1, style: { maxWidth: 480, width: "100%" }, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 5, children: [
|
|
49
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, radius: "full", tone: "primary", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 4, children: /* @__PURE__ */ jsxRuntime.jsx(icons.EyeOpenIcon, {}) }) }) }),
|
|
50
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, style: { textAlign: "center" }, children: [
|
|
51
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Preview Mode Authentication" }),
|
|
52
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, size: 2, children: "Authenticate to access the preview environment. This generates a secure token valid for 3 months." })
|
|
53
|
+
] }),
|
|
54
|
+
fullRedirectUrl && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, radius: 2, tone: "positive", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
|
|
55
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, muted: !0, weight: "medium", children: "Redirecting to:" }),
|
|
56
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, style: { wordBreak: "break-all" }, children: fullRedirectUrl })
|
|
57
|
+
] }) }),
|
|
58
|
+
error && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, radius: 2, tone: "critical", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: error }) }),
|
|
59
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
60
|
+
ui.Button,
|
|
61
|
+
{
|
|
62
|
+
fontSize: 2,
|
|
63
|
+
padding: 4,
|
|
64
|
+
tone: "primary",
|
|
65
|
+
loading: isAuthenticating,
|
|
66
|
+
text: isAuthenticating ? "Authenticating\u2026" : "Authenticate Preview Mode",
|
|
67
|
+
icon: icons.EyeOpenIcon,
|
|
68
|
+
onClick: handleAuthenticate,
|
|
69
|
+
disabled: isAuthenticating || !previewOrigin
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
] }) }) }) }) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { height: "fill", overflow: "auto", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { width: 1, padding: 5, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "center", align: "center", style: { minHeight: "60vh" }, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 5, radius: 3, shadow: 1, style: { maxWidth: 480, width: "100%" }, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, style: { textAlign: "center" }, children: [
|
|
73
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 4, children: "404" }),
|
|
74
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, size: 2, children: "Preview authentication requires a redirect URL." })
|
|
75
|
+
] }) }) }) }) });
|
|
76
|
+
}
|
|
77
|
+
const previewAuthSecret = sanity.defineType({
|
|
78
|
+
name: sanityPluginPreviewAuthValidate.PREVIEW_AUTH_SECRET_TYPE,
|
|
79
|
+
type: "document",
|
|
80
|
+
title: "Preview Auth Secret",
|
|
81
|
+
fields: [
|
|
82
|
+
sanity.defineField({ name: "secret", type: "string", title: "Secret" }),
|
|
83
|
+
sanity.defineField({ name: "source", type: "string", title: "Source" }),
|
|
84
|
+
sanity.defineField({ name: "studioUrl", type: "string", title: "Studio URL" }),
|
|
85
|
+
sanity.defineField({ name: "userId", type: "string", title: "User ID" }),
|
|
86
|
+
sanity.defineField({ name: "expiresAt", type: "datetime", title: "Expires At" })
|
|
87
|
+
]
|
|
88
|
+
}), TOOL_NAME = "preview-auth", previewAuthPlugin = sanity.definePlugin((options) => ({
|
|
89
|
+
name: TOOL_NAME,
|
|
90
|
+
schema: {
|
|
91
|
+
types: [previewAuthSecret]
|
|
92
|
+
},
|
|
93
|
+
tools: [
|
|
94
|
+
{
|
|
95
|
+
name: TOOL_NAME,
|
|
96
|
+
title: "Preview Auth",
|
|
97
|
+
icon: icons.EyeOpenIcon,
|
|
98
|
+
component: () => PreviewAuthPage(options)
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
studio: {
|
|
102
|
+
components: {
|
|
103
|
+
toolMenu: (props) => {
|
|
104
|
+
const filteredTools = props.tools.filter((tool) => tool.name !== TOOL_NAME);
|
|
105
|
+
return props.renderDefault({ ...props, tools: filteredTools });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}));
|
|
110
|
+
function createAction(options) {
|
|
111
|
+
const { previewOrigin, slugField = "slug" } = options, OpenPreviewAction = (props) => {
|
|
112
|
+
const client = sanity.useClient(sanity.DEFAULT_STUDIO_CLIENT_OPTIONS), currentUser = sanity.useCurrentUser(), [isGenerating, setIsGenerating] = react.useState(!1), slug = (props.draft ?? props.published)?.[slugField], slugValue = typeof slug == "string" ? slug : slug?.current;
|
|
113
|
+
return slugValue ? {
|
|
114
|
+
label: isGenerating ? "Opening\u2026" : "Open Preview",
|
|
115
|
+
icon: icons.EyeOpenIcon,
|
|
116
|
+
disabled: isGenerating,
|
|
117
|
+
onHandle: async () => {
|
|
118
|
+
setIsGenerating(!0);
|
|
119
|
+
try {
|
|
120
|
+
const { secret } = await sanityPluginPreviewAuthValidate.createPreviewSecret({
|
|
121
|
+
client,
|
|
122
|
+
source: "sanity/open-preview-action",
|
|
123
|
+
studioUrl: window.location.href,
|
|
124
|
+
userId: currentUser?.id
|
|
125
|
+
}), previewPath = slugValue.startsWith("/") ? slugValue : `/${slugValue}`, url = new URL(`/preview${previewPath}`, previewOrigin);
|
|
126
|
+
url.searchParams.set("sanity-preview-secret", secret), window.open(url.toString(), "_blank", "noopener,noreferrer");
|
|
127
|
+
} finally {
|
|
128
|
+
setIsGenerating(!1), props.onComplete();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} : null;
|
|
132
|
+
};
|
|
133
|
+
return OpenPreviewAction.action = "openPreview", OpenPreviewAction;
|
|
134
|
+
}
|
|
135
|
+
function openPreviewAction(options) {
|
|
136
|
+
return createAction(options);
|
|
137
|
+
}
|
|
138
|
+
exports.openPreviewAction = openPreviewAction;
|
|
139
|
+
exports.previewAuthPlugin = previewAuthPlugin;
|
|
140
|
+
exports.previewAuthSecret = previewAuthSecret;
|
|
141
|
+
exports.usePreviewAuth = usePreviewAuth;
|
|
142
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/hooks/usePreviewAuth.ts","../src/components/PreviewAuthPage.tsx","../src/schema/previewAuthSecret.ts","../src/plugin.ts","../src/actions/openPreviewAction.ts"],"sourcesContent":["import { useState, useCallback, useMemo } from 'react'\nimport { useClient, useCurrentUser, DEFAULT_STUDIO_CLIENT_OPTIONS } from 'sanity'\nimport { createPreviewSecret } from 'sanity-plugin-preview-auth-validate'\n\n/** @public */\nexport type UsePreviewAuthOptions = {\n\tpreviewOrigin: string\n\tpreviewAuthApi: string\n}\n\n/** @public */\nexport type UsePreviewAuthResult = {\n\tredirectUrl: string | null\n\tfullRedirectUrl: string | null\n\tisAuthenticating: boolean\n\terror: string | null\n\thandleAuthenticate: () => Promise<void>\n}\n\n/** @public */\nexport function usePreviewAuth({\n\tpreviewOrigin,\n\tpreviewAuthApi,\n}: UsePreviewAuthOptions): UsePreviewAuthResult {\n\tconst client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)\n const currentUser = useCurrentUser()\n \n\tconst [isAuthenticating, setIsAuthenticating] = useState(false)\n\tconst [error, setError] = useState<string | null>(null)\n\n\tconst redirectUrl = useMemo(() => {\n\t\tif (typeof window === 'undefined') {\n\t\t\treturn null\n\t\t}\n\n\t\treturn new URLSearchParams(window.location.search).get('redirect')\n\t}, [])\n\n\tconst fullRedirectUrl = useMemo(() => {\n\t\tif (!redirectUrl || !previewOrigin) {\n\t\t\treturn null\n\t\t}\n\n\t\ttry {\n\t\t\treturn new URL(redirectUrl, previewOrigin).toString()\n\t\t} catch {\n\t\t\treturn `${previewOrigin}${redirectUrl}`\n\t\t}\n\t}, [redirectUrl, previewOrigin])\n\n\tconst handleAuthenticate = useCallback(async () => {\n\t\tif (!previewOrigin) {\n\t\t\tsetError('No preview URL configured for this workspace.')\n\n\t\t\treturn\n\t\t}\n\n\t\tsetIsAuthenticating(true)\n\t\tsetError(null)\n\n\t\ttry {\n\t\t\tconst { secret } = await createPreviewSecret({\n\t\t\t\tclient,\n\t\t\t\tsource: 'sanity/preview-auth',\n\t\t\t\tstudioUrl: window.location.href,\n\t\t\t\tuserId: currentUser?.id,\n\t\t\t})\n\n\t\t\tconst apiUrl = new URL(previewAuthApi, previewOrigin)\n\t\t\tconst redirectPath = redirectUrl?.startsWith('/preview')\n\t\t\t\t? redirectUrl.replace(/^\\/preview/, '') || '/'\n\t\t\t\t: redirectUrl || '/'\n\n\t\t\tapiUrl.searchParams.set('sanity-preview-secret', secret)\n\t\t\tapiUrl.searchParams.set('sanity-preview-pathname', redirectPath)\n\t\t\tapiUrl.searchParams.set('response', 'json')\n\n\t\t\tconst response = await fetch(apiUrl, {\n\t\t\t\tmethod: 'GET',\n\t\t\t\tcredentials: 'include',\n\t\t\t\theaders: { Accept: 'application/json' },\n\t\t\t})\n\n\t\t\tconst payload = await response.json().catch(() => null)\n\n\t\t\tif (!response.ok || payload?.ok === false) {\n\t\t\t\tthrow new Error(payload?.error || `Authentication failed (${response.status})`)\n\t\t\t}\n\n\t\t\tif (!payload?.redirectTo) {\n\t\t\t\tthrow new Error('Missing redirect target from authentication response.')\n\t\t\t}\n\n\t\t\twindow.location.href = new URL(payload.redirectTo, previewOrigin).toString()\n\t\t} catch (err) {\n\t\t\tsetError(err instanceof Error ? err.message : 'Failed to authenticate preview mode.')\n\t\t\tsetIsAuthenticating(false)\n\t\t}\n\t}, [client, currentUser?.id, previewAuthApi, previewOrigin, redirectUrl])\n\n\treturn { redirectUrl, fullRedirectUrl, isAuthenticating, error, handleAuthenticate }\n}\n","import React from 'react'\nimport { Button, Card, Container, Flex, Heading, Stack, Text } from '@sanity/ui'\nimport { EyeOpenIcon } from '@sanity/icons'\nimport { usePreviewAuth } from '../hooks/usePreviewAuth'\n\nexport type PreviewAuthPageProps = {\n\tpreviewOrigin: string\n\tpreviewAuthApi: string\n}\n\nexport function PreviewAuthPage({ previewOrigin, previewAuthApi }: PreviewAuthPageProps) {\n\tconst { redirectUrl, fullRedirectUrl, isAuthenticating, error, handleAuthenticate } =\n\t\tusePreviewAuth({ previewOrigin, previewAuthApi })\n\n\tif (!redirectUrl) {\n\t\treturn (\n\t\t\t<Card height='fill' overflow='auto'>\n\t\t\t\t<Container width={1} padding={5}>\n\t\t\t\t\t<Flex justify='center' align='center' style={{ minHeight: '60vh' }}>\n\t\t\t\t\t\t<Card padding={5} radius={3} shadow={1} style={{ maxWidth: 480, width: '100%' }}>\n\t\t\t\t\t\t\t<Stack space={4} style={{ textAlign: 'center' }}>\n\t\t\t\t\t\t\t\t<Heading size={4}>404</Heading>\n\t\t\t\t\t\t\t\t<Text muted size={2}>\n\t\t\t\t\t\t\t\t\tPreview authentication requires a redirect URL.\n\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</Flex>\n\t\t\t\t</Container>\n\t\t\t</Card>\n\t\t)\n\t}\n\n\treturn (\n\t\t<Card height='fill' overflow='auto'>\n\t\t\t<Container width={1} padding={5}>\n\t\t\t\t<Flex justify='center' align='center' style={{ minHeight: '60vh' }}>\n\t\t\t\t\t<Card padding={5} radius={3} shadow={1} style={{ maxWidth: 480, width: '100%' }}>\n\t\t\t\t\t\t<Stack space={5}>\n\t\t\t\t\t\t\t<Flex justify='center'>\n\t\t\t\t\t\t\t\t<Card padding={3} radius='full' tone='primary'>\n\t\t\t\t\t\t\t\t\t<Text size={4}>\n\t\t\t\t\t\t\t\t\t\t<EyeOpenIcon />\n\t\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t</Flex>\n\n\t\t\t\t\t\t\t<Stack space={3} style={{ textAlign: 'center' }}>\n\t\t\t\t\t\t\t\t<Heading size={2}>Preview Mode Authentication</Heading>\n\t\t\t\t\t\t\t\t<Text muted size={2}>\n\t\t\t\t\t\t\t\t\tAuthenticate to access the preview environment. This generates a secure token\n\t\t\t\t\t\t\t\t\tvalid for 3 months.\n\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t</Stack>\n\n\t\t\t\t\t\t\t{fullRedirectUrl && (\n\t\t\t\t\t\t\t\t<Card padding={3} radius={2} tone='positive'>\n\t\t\t\t\t\t\t\t\t<Stack space={3}>\n\t\t\t\t\t\t\t\t\t\t<Text size={0} muted weight='medium'>\n\t\t\t\t\t\t\t\t\t\t\tRedirecting to:\n\t\t\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t\t\t\t<Text size={1} style={{ wordBreak: 'break-all' }}>\n\t\t\t\t\t\t\t\t\t\t\t{fullRedirectUrl}\n\t\t\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t{error && (\n\t\t\t\t\t\t\t\t<Card padding={3} radius={2} tone='critical'>\n\t\t\t\t\t\t\t\t\t<Text size={1}>{error}</Text>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\tfontSize={2}\n\t\t\t\t\t\t\t\tpadding={4}\n\t\t\t\t\t\t\t\ttone='primary'\n\t\t\t\t\t\t\t\tloading={isAuthenticating}\n\t\t\t\t\t\t\t\ttext={isAuthenticating ? 'Authenticating…' : 'Authenticate Preview Mode'}\n\t\t\t\t\t\t\t\ticon={EyeOpenIcon}\n\t\t\t\t\t\t\t\tonClick={handleAuthenticate}\n\t\t\t\t\t\t\t\tdisabled={isAuthenticating || !previewOrigin}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t</Card>\n\t\t\t\t</Flex>\n\t\t\t</Container>\n\t\t</Card>\n\t)\n}\n","import { defineField, defineType } from 'sanity';\nimport { PREVIEW_AUTH_SECRET_TYPE } from 'sanity-plugin-preview-auth-validate';\n\n/**\n * System document type for long-lived preview auth secrets.\n * Add this to your Sanity schema types array, or use `previewAuthPlugin`\n * which registers it automatically.\n * @public\n */\nexport const previewAuthSecret = defineType({\n name: PREVIEW_AUTH_SECRET_TYPE,\n type: 'document',\n title: 'Preview Auth Secret',\n fields: [\n defineField({ name: 'secret', type: 'string', title: 'Secret' }),\n defineField({ name: 'source', type: 'string', title: 'Source' }),\n defineField({ name: 'studioUrl', type: 'string', title: 'Studio URL' }),\n defineField({ name: 'userId', type: 'string', title: 'User ID' }),\n defineField({ name: 'expiresAt', type: 'datetime', title: 'Expires At' })\n ]\n});\n","import { definePlugin } from 'sanity';\nimport { EyeOpenIcon } from '@sanity/icons';\nimport { PreviewAuthPage } from './components/PreviewAuthPage';\nimport { previewAuthSecret } from './schema/previewAuthSecret';\n\n/** @public */\nexport type PreviewAuthPluginOptions = {\n /** The origin of your preview site, e.g. https://preview.mysite.com */\n previewOrigin: string;\n /** The path to your draft-mode enable API, e.g. /api/draft-mode/enable */\n previewAuthApi: string;\n};\n\nconst TOOL_NAME = 'preview-auth';\n\n/**\n * Sanity Studio plugin for long-lived cross-origin preview authentication.\n *\n * Registers:\n * - A hidden `preview-auth` tool reachable via `/preview-auth?redirect=…`\n * - The `sanity.previewAuthSecret` schema type for long-lived secrets\n * @public\n */\nexport const previewAuthPlugin = definePlugin<PreviewAuthPluginOptions>((options) => ({\n name: TOOL_NAME,\n schema: {\n types: [previewAuthSecret]\n },\n tools: [\n {\n name: TOOL_NAME,\n title: 'Preview Auth',\n icon: EyeOpenIcon,\n component: () => PreviewAuthPage(options)\n }\n ],\n studio: {\n components: {\n toolMenu: (props) => {\n const filteredTools = props.tools.filter((tool) => tool.name !== TOOL_NAME);\n\n return props.renderDefault({ ...props, tools: filteredTools });\n }\n }\n }\n}));\n","import React, { useState } from 'react'\nimport { useClient, useCurrentUser, DEFAULT_STUDIO_CLIENT_OPTIONS } from 'sanity'\nimport type { DocumentActionComponent, DocumentActionProps } from 'sanity'\nimport { EyeOpenIcon } from '@sanity/icons'\nimport { createPreviewSecret } from 'sanity-plugin-preview-auth-validate'\n\n/** @public */\nexport type OpenPreviewActionOptions = {\n\tpreviewOrigin: string\n\t/** Slug field name on route documents (default: 'slug') */\n\tslugField?: string\n}\n\nfunction createAction(options: OpenPreviewActionOptions): DocumentActionComponent {\n\tconst { previewOrigin, slugField = 'slug' } = options\n\n\tconst OpenPreviewAction = (props: DocumentActionProps) => {\n\t\tconst client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)\n\t\tconst currentUser = useCurrentUser()\n\t\tconst [isGenerating, setIsGenerating] = useState(false)\n\n\t\tconst slug = (props.draft ?? props.published)?.[slugField] as\n\t\t\t| { current?: string }\n\t\t\t| string\n\t\t\t| undefined\n\n\t\tconst slugValue = typeof slug === 'string' ? slug : slug?.current\n\n\t\tif (!slugValue) {\n\t\t\treturn null\n\t\t}\n\n\t\treturn {\n\t\t\tlabel: isGenerating ? 'Opening…' : 'Open Preview',\n\t\t\ticon: EyeOpenIcon,\n\t\t\tdisabled: isGenerating,\n\t\t\tonHandle: async () => {\n\t\t\t\tsetIsGenerating(true)\n\n\t\t\t\ttry {\n\t\t\t\t\tconst { secret } = await createPreviewSecret({\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tsource: 'sanity/open-preview-action',\n\t\t\t\t\t\tstudioUrl: window.location.href,\n\t\t\t\t\t\tuserId: currentUser?.id,\n\t\t\t\t\t})\n\n\t\t\t\t\tconst previewPath = slugValue.startsWith('/') ? slugValue : `/${slugValue}`\n\t\t\t\t\tconst url = new URL(`/preview${previewPath}`, previewOrigin)\n\n\t\t\t\t\turl.searchParams.set('sanity-preview-secret', secret)\n\n\t\t\t\t\twindow.open(url.toString(), '_blank', 'noopener,noreferrer')\n\t\t\t\t} finally {\n\t\t\t\t\tsetIsGenerating(false)\n\t\t\t\t\tprops.onComplete()\n\t\t\t\t}\n\t\t\t},\n\t\t}\n\t}\n\n\tOpenPreviewAction.action = 'openPreview'\n\n\treturn OpenPreviewAction as DocumentActionComponent\n}\n\n/** @public */\nexport function openPreviewAction(options: OpenPreviewActionOptions): DocumentActionComponent {\n\treturn createAction(options)\n}\n"],"names":["useClient","DEFAULT_STUDIO_CLIENT_OPTIONS","useCurrentUser","useState","useMemo","useCallback","createPreviewSecret","jsx","Card","Container","Flex","jsxs","Stack","Text","EyeOpenIcon","Heading","Button","defineType","PREVIEW_AUTH_SECRET_TYPE","defineField","definePlugin"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { DocumentActionComponent } from "sanity";
|
|
2
|
+
import { DocumentDefinition } from "sanity";
|
|
3
|
+
import { Plugin as Plugin_2 } from "sanity";
|
|
4
|
+
import { PreviewConfig } from "sanity";
|
|
5
|
+
|
|
6
|
+
/** @public */
|
|
7
|
+
export declare function openPreviewAction(
|
|
8
|
+
options: OpenPreviewActionOptions,
|
|
9
|
+
): DocumentActionComponent;
|
|
10
|
+
|
|
11
|
+
/** @public */
|
|
12
|
+
export declare type OpenPreviewActionOptions = {
|
|
13
|
+
previewOrigin: string;
|
|
14
|
+
/** Slug field name on route documents (default: 'slug') */
|
|
15
|
+
slugField?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sanity Studio plugin for long-lived cross-origin preview authentication.
|
|
20
|
+
*
|
|
21
|
+
* Registers:
|
|
22
|
+
* - A hidden `preview-auth` tool reachable via `/preview-auth?redirect=…`
|
|
23
|
+
* - The `sanity.previewAuthSecret` schema type for long-lived secrets
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
export declare const previewAuthPlugin: Plugin_2<PreviewAuthPluginOptions>;
|
|
27
|
+
|
|
28
|
+
/** @public */
|
|
29
|
+
export declare type PreviewAuthPluginOptions = {
|
|
30
|
+
/** The origin of your preview site, e.g. https://preview.mysite.com */
|
|
31
|
+
previewOrigin: string;
|
|
32
|
+
/** The path to your draft-mode enable API, e.g. /api/draft-mode/enable */
|
|
33
|
+
previewAuthApi: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* System document type for long-lived preview auth secrets.
|
|
38
|
+
* Add this to your Sanity schema types array, or use `previewAuthPlugin`
|
|
39
|
+
* which registers it automatically.
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
export declare const previewAuthSecret: {
|
|
43
|
+
type: "document";
|
|
44
|
+
name: "sanity.previewAuthSecret";
|
|
45
|
+
} & Omit<DocumentDefinition, "preview"> & {
|
|
46
|
+
preview?:
|
|
47
|
+
| PreviewConfig<Record<string, string>, Record<never, any>>
|
|
48
|
+
| undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** @public */
|
|
52
|
+
export declare function usePreviewAuth({
|
|
53
|
+
previewOrigin,
|
|
54
|
+
previewAuthApi,
|
|
55
|
+
}: UsePreviewAuthOptions): UsePreviewAuthResult;
|
|
56
|
+
|
|
57
|
+
/** @public */
|
|
58
|
+
export declare type UsePreviewAuthOptions = {
|
|
59
|
+
previewOrigin: string;
|
|
60
|
+
previewAuthApi: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** @public */
|
|
64
|
+
export declare type UsePreviewAuthResult = {
|
|
65
|
+
redirectUrl: string | null;
|
|
66
|
+
fullRedirectUrl: string | null;
|
|
67
|
+
isAuthenticating: boolean;
|
|
68
|
+
error: string | null;
|
|
69
|
+
handleAuthenticate: () => Promise<void>;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { DocumentActionComponent } from "sanity";
|
|
2
|
+
import { DocumentDefinition } from "sanity";
|
|
3
|
+
import { Plugin as Plugin_2 } from "sanity";
|
|
4
|
+
import { PreviewConfig } from "sanity";
|
|
5
|
+
|
|
6
|
+
/** @public */
|
|
7
|
+
export declare function openPreviewAction(
|
|
8
|
+
options: OpenPreviewActionOptions,
|
|
9
|
+
): DocumentActionComponent;
|
|
10
|
+
|
|
11
|
+
/** @public */
|
|
12
|
+
export declare type OpenPreviewActionOptions = {
|
|
13
|
+
previewOrigin: string;
|
|
14
|
+
/** Slug field name on route documents (default: 'slug') */
|
|
15
|
+
slugField?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sanity Studio plugin for long-lived cross-origin preview authentication.
|
|
20
|
+
*
|
|
21
|
+
* Registers:
|
|
22
|
+
* - A hidden `preview-auth` tool reachable via `/preview-auth?redirect=…`
|
|
23
|
+
* - The `sanity.previewAuthSecret` schema type for long-lived secrets
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
export declare const previewAuthPlugin: Plugin_2<PreviewAuthPluginOptions>;
|
|
27
|
+
|
|
28
|
+
/** @public */
|
|
29
|
+
export declare type PreviewAuthPluginOptions = {
|
|
30
|
+
/** The origin of your preview site, e.g. https://preview.mysite.com */
|
|
31
|
+
previewOrigin: string;
|
|
32
|
+
/** The path to your draft-mode enable API, e.g. /api/draft-mode/enable */
|
|
33
|
+
previewAuthApi: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* System document type for long-lived preview auth secrets.
|
|
38
|
+
* Add this to your Sanity schema types array, or use `previewAuthPlugin`
|
|
39
|
+
* which registers it automatically.
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
42
|
+
export declare const previewAuthSecret: {
|
|
43
|
+
type: "document";
|
|
44
|
+
name: "sanity.previewAuthSecret";
|
|
45
|
+
} & Omit<DocumentDefinition, "preview"> & {
|
|
46
|
+
preview?:
|
|
47
|
+
| PreviewConfig<Record<string, string>, Record<never, any>>
|
|
48
|
+
| undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** @public */
|
|
52
|
+
export declare function usePreviewAuth({
|
|
53
|
+
previewOrigin,
|
|
54
|
+
previewAuthApi,
|
|
55
|
+
}: UsePreviewAuthOptions): UsePreviewAuthResult;
|
|
56
|
+
|
|
57
|
+
/** @public */
|
|
58
|
+
export declare type UsePreviewAuthOptions = {
|
|
59
|
+
previewOrigin: string;
|
|
60
|
+
previewAuthApi: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** @public */
|
|
64
|
+
export declare type UsePreviewAuthResult = {
|
|
65
|
+
redirectUrl: string | null;
|
|
66
|
+
fullRedirectUrl: string | null;
|
|
67
|
+
isAuthenticating: boolean;
|
|
68
|
+
error: string | null;
|
|
69
|
+
handleAuthenticate: () => Promise<void>;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useClient, DEFAULT_STUDIO_CLIENT_OPTIONS, useCurrentUser, defineType, defineField, definePlugin } from "sanity";
|
|
2
|
+
import { EyeOpenIcon } from "@sanity/icons";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { Card, Container, Flex, Stack, Text, Heading, Button } from "@sanity/ui";
|
|
5
|
+
import { useState, useMemo, useCallback } from "react";
|
|
6
|
+
import { createPreviewSecret, PREVIEW_AUTH_SECRET_TYPE } from "sanity-plugin-preview-auth-validate";
|
|
7
|
+
function usePreviewAuth({
|
|
8
|
+
previewOrigin,
|
|
9
|
+
previewAuthApi
|
|
10
|
+
}) {
|
|
11
|
+
const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS), currentUser = useCurrentUser(), [isAuthenticating, setIsAuthenticating] = useState(!1), [error, setError] = useState(null), redirectUrl = useMemo(() => typeof window > "u" ? null : new URLSearchParams(window.location.search).get("redirect"), []), fullRedirectUrl = useMemo(() => {
|
|
12
|
+
if (!redirectUrl || !previewOrigin)
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
return new URL(redirectUrl, previewOrigin).toString();
|
|
16
|
+
} catch {
|
|
17
|
+
return `${previewOrigin}${redirectUrl}`;
|
|
18
|
+
}
|
|
19
|
+
}, [redirectUrl, previewOrigin]), handleAuthenticate = useCallback(async () => {
|
|
20
|
+
if (!previewOrigin) {
|
|
21
|
+
setError("No preview URL configured for this workspace.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
setIsAuthenticating(!0), setError(null);
|
|
25
|
+
try {
|
|
26
|
+
const { secret } = await createPreviewSecret({
|
|
27
|
+
client,
|
|
28
|
+
source: "sanity/preview-auth",
|
|
29
|
+
studioUrl: window.location.href,
|
|
30
|
+
userId: currentUser?.id
|
|
31
|
+
}), apiUrl = new URL(previewAuthApi, previewOrigin), redirectPath = redirectUrl?.startsWith("/preview") ? redirectUrl.replace(/^\/preview/, "") || "/" : redirectUrl || "/";
|
|
32
|
+
apiUrl.searchParams.set("sanity-preview-secret", secret), apiUrl.searchParams.set("sanity-preview-pathname", redirectPath), apiUrl.searchParams.set("response", "json");
|
|
33
|
+
const response = await fetch(apiUrl, {
|
|
34
|
+
method: "GET",
|
|
35
|
+
credentials: "include",
|
|
36
|
+
headers: { Accept: "application/json" }
|
|
37
|
+
}), payload = await response.json().catch(() => null);
|
|
38
|
+
if (!response.ok || payload?.ok === !1)
|
|
39
|
+
throw new Error(payload?.error || `Authentication failed (${response.status})`);
|
|
40
|
+
if (!payload?.redirectTo)
|
|
41
|
+
throw new Error("Missing redirect target from authentication response.");
|
|
42
|
+
window.location.href = new URL(payload.redirectTo, previewOrigin).toString();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
setError(err instanceof Error ? err.message : "Failed to authenticate preview mode."), setIsAuthenticating(!1);
|
|
45
|
+
}
|
|
46
|
+
}, [client, currentUser?.id, previewAuthApi, previewOrigin, redirectUrl]);
|
|
47
|
+
return { redirectUrl, fullRedirectUrl, isAuthenticating, error, handleAuthenticate };
|
|
48
|
+
}
|
|
49
|
+
function PreviewAuthPage({ previewOrigin, previewAuthApi }) {
|
|
50
|
+
const { redirectUrl, fullRedirectUrl, isAuthenticating, error, handleAuthenticate } = usePreviewAuth({ previewOrigin, previewAuthApi });
|
|
51
|
+
return redirectUrl ? /* @__PURE__ */ jsx(Card, { height: "fill", overflow: "auto", children: /* @__PURE__ */ jsx(Container, { width: 1, padding: 5, children: /* @__PURE__ */ jsx(Flex, { justify: "center", align: "center", style: { minHeight: "60vh" }, children: /* @__PURE__ */ jsx(Card, { padding: 5, radius: 3, shadow: 1, style: { maxWidth: 480, width: "100%" }, children: /* @__PURE__ */ jsxs(Stack, { space: 5, children: [
|
|
52
|
+
/* @__PURE__ */ jsx(Flex, { justify: "center", children: /* @__PURE__ */ jsx(Card, { padding: 3, radius: "full", tone: "primary", children: /* @__PURE__ */ jsx(Text, { size: 4, children: /* @__PURE__ */ jsx(EyeOpenIcon, {}) }) }) }),
|
|
53
|
+
/* @__PURE__ */ jsxs(Stack, { space: 3, style: { textAlign: "center" }, children: [
|
|
54
|
+
/* @__PURE__ */ jsx(Heading, { size: 2, children: "Preview Mode Authentication" }),
|
|
55
|
+
/* @__PURE__ */ jsx(Text, { muted: !0, size: 2, children: "Authenticate to access the preview environment. This generates a secure token valid for 3 months." })
|
|
56
|
+
] }),
|
|
57
|
+
fullRedirectUrl && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "positive", children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
|
|
58
|
+
/* @__PURE__ */ jsx(Text, { size: 0, muted: !0, weight: "medium", children: "Redirecting to:" }),
|
|
59
|
+
/* @__PURE__ */ jsx(Text, { size: 1, style: { wordBreak: "break-all" }, children: fullRedirectUrl })
|
|
60
|
+
] }) }),
|
|
61
|
+
error && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "critical", children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) }),
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
Button,
|
|
64
|
+
{
|
|
65
|
+
fontSize: 2,
|
|
66
|
+
padding: 4,
|
|
67
|
+
tone: "primary",
|
|
68
|
+
loading: isAuthenticating,
|
|
69
|
+
text: isAuthenticating ? "Authenticating\u2026" : "Authenticate Preview Mode",
|
|
70
|
+
icon: EyeOpenIcon,
|
|
71
|
+
onClick: handleAuthenticate,
|
|
72
|
+
disabled: isAuthenticating || !previewOrigin
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
] }) }) }) }) }) : /* @__PURE__ */ jsx(Card, { height: "fill", overflow: "auto", children: /* @__PURE__ */ jsx(Container, { width: 1, padding: 5, children: /* @__PURE__ */ jsx(Flex, { justify: "center", align: "center", style: { minHeight: "60vh" }, children: /* @__PURE__ */ jsx(Card, { padding: 5, radius: 3, shadow: 1, style: { maxWidth: 480, width: "100%" }, children: /* @__PURE__ */ jsxs(Stack, { space: 4, style: { textAlign: "center" }, children: [
|
|
76
|
+
/* @__PURE__ */ jsx(Heading, { size: 4, children: "404" }),
|
|
77
|
+
/* @__PURE__ */ jsx(Text, { muted: !0, size: 2, children: "Preview authentication requires a redirect URL." })
|
|
78
|
+
] }) }) }) }) });
|
|
79
|
+
}
|
|
80
|
+
const previewAuthSecret = defineType({
|
|
81
|
+
name: PREVIEW_AUTH_SECRET_TYPE,
|
|
82
|
+
type: "document",
|
|
83
|
+
title: "Preview Auth Secret",
|
|
84
|
+
fields: [
|
|
85
|
+
defineField({ name: "secret", type: "string", title: "Secret" }),
|
|
86
|
+
defineField({ name: "source", type: "string", title: "Source" }),
|
|
87
|
+
defineField({ name: "studioUrl", type: "string", title: "Studio URL" }),
|
|
88
|
+
defineField({ name: "userId", type: "string", title: "User ID" }),
|
|
89
|
+
defineField({ name: "expiresAt", type: "datetime", title: "Expires At" })
|
|
90
|
+
]
|
|
91
|
+
}), TOOL_NAME = "preview-auth", previewAuthPlugin = definePlugin((options) => ({
|
|
92
|
+
name: TOOL_NAME,
|
|
93
|
+
schema: {
|
|
94
|
+
types: [previewAuthSecret]
|
|
95
|
+
},
|
|
96
|
+
tools: [
|
|
97
|
+
{
|
|
98
|
+
name: TOOL_NAME,
|
|
99
|
+
title: "Preview Auth",
|
|
100
|
+
icon: EyeOpenIcon,
|
|
101
|
+
component: () => PreviewAuthPage(options)
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
studio: {
|
|
105
|
+
components: {
|
|
106
|
+
toolMenu: (props) => {
|
|
107
|
+
const filteredTools = props.tools.filter((tool) => tool.name !== TOOL_NAME);
|
|
108
|
+
return props.renderDefault({ ...props, tools: filteredTools });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}));
|
|
113
|
+
function createAction(options) {
|
|
114
|
+
const { previewOrigin, slugField = "slug" } = options, OpenPreviewAction = (props) => {
|
|
115
|
+
const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS), currentUser = useCurrentUser(), [isGenerating, setIsGenerating] = useState(!1), slug = (props.draft ?? props.published)?.[slugField], slugValue = typeof slug == "string" ? slug : slug?.current;
|
|
116
|
+
return slugValue ? {
|
|
117
|
+
label: isGenerating ? "Opening\u2026" : "Open Preview",
|
|
118
|
+
icon: EyeOpenIcon,
|
|
119
|
+
disabled: isGenerating,
|
|
120
|
+
onHandle: async () => {
|
|
121
|
+
setIsGenerating(!0);
|
|
122
|
+
try {
|
|
123
|
+
const { secret } = await createPreviewSecret({
|
|
124
|
+
client,
|
|
125
|
+
source: "sanity/open-preview-action",
|
|
126
|
+
studioUrl: window.location.href,
|
|
127
|
+
userId: currentUser?.id
|
|
128
|
+
}), previewPath = slugValue.startsWith("/") ? slugValue : `/${slugValue}`, url = new URL(`/preview${previewPath}`, previewOrigin);
|
|
129
|
+
url.searchParams.set("sanity-preview-secret", secret), window.open(url.toString(), "_blank", "noopener,noreferrer");
|
|
130
|
+
} finally {
|
|
131
|
+
setIsGenerating(!1), props.onComplete();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} : null;
|
|
135
|
+
};
|
|
136
|
+
return OpenPreviewAction.action = "openPreview", OpenPreviewAction;
|
|
137
|
+
}
|
|
138
|
+
function openPreviewAction(options) {
|
|
139
|
+
return createAction(options);
|
|
140
|
+
}
|
|
141
|
+
export {
|
|
142
|
+
openPreviewAction,
|
|
143
|
+
previewAuthPlugin,
|
|
144
|
+
previewAuthSecret,
|
|
145
|
+
usePreviewAuth
|
|
146
|
+
};
|
|
147
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/hooks/usePreviewAuth.ts","../src/components/PreviewAuthPage.tsx","../src/schema/previewAuthSecret.ts","../src/plugin.ts","../src/actions/openPreviewAction.ts"],"sourcesContent":["import { useState, useCallback, useMemo } from 'react'\nimport { useClient, useCurrentUser, DEFAULT_STUDIO_CLIENT_OPTIONS } from 'sanity'\nimport { createPreviewSecret } from 'sanity-plugin-preview-auth-validate'\n\n/** @public */\nexport type UsePreviewAuthOptions = {\n\tpreviewOrigin: string\n\tpreviewAuthApi: string\n}\n\n/** @public */\nexport type UsePreviewAuthResult = {\n\tredirectUrl: string | null\n\tfullRedirectUrl: string | null\n\tisAuthenticating: boolean\n\terror: string | null\n\thandleAuthenticate: () => Promise<void>\n}\n\n/** @public */\nexport function usePreviewAuth({\n\tpreviewOrigin,\n\tpreviewAuthApi,\n}: UsePreviewAuthOptions): UsePreviewAuthResult {\n\tconst client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)\n const currentUser = useCurrentUser()\n \n\tconst [isAuthenticating, setIsAuthenticating] = useState(false)\n\tconst [error, setError] = useState<string | null>(null)\n\n\tconst redirectUrl = useMemo(() => {\n\t\tif (typeof window === 'undefined') {\n\t\t\treturn null\n\t\t}\n\n\t\treturn new URLSearchParams(window.location.search).get('redirect')\n\t}, [])\n\n\tconst fullRedirectUrl = useMemo(() => {\n\t\tif (!redirectUrl || !previewOrigin) {\n\t\t\treturn null\n\t\t}\n\n\t\ttry {\n\t\t\treturn new URL(redirectUrl, previewOrigin).toString()\n\t\t} catch {\n\t\t\treturn `${previewOrigin}${redirectUrl}`\n\t\t}\n\t}, [redirectUrl, previewOrigin])\n\n\tconst handleAuthenticate = useCallback(async () => {\n\t\tif (!previewOrigin) {\n\t\t\tsetError('No preview URL configured for this workspace.')\n\n\t\t\treturn\n\t\t}\n\n\t\tsetIsAuthenticating(true)\n\t\tsetError(null)\n\n\t\ttry {\n\t\t\tconst { secret } = await createPreviewSecret({\n\t\t\t\tclient,\n\t\t\t\tsource: 'sanity/preview-auth',\n\t\t\t\tstudioUrl: window.location.href,\n\t\t\t\tuserId: currentUser?.id,\n\t\t\t})\n\n\t\t\tconst apiUrl = new URL(previewAuthApi, previewOrigin)\n\t\t\tconst redirectPath = redirectUrl?.startsWith('/preview')\n\t\t\t\t? redirectUrl.replace(/^\\/preview/, '') || '/'\n\t\t\t\t: redirectUrl || '/'\n\n\t\t\tapiUrl.searchParams.set('sanity-preview-secret', secret)\n\t\t\tapiUrl.searchParams.set('sanity-preview-pathname', redirectPath)\n\t\t\tapiUrl.searchParams.set('response', 'json')\n\n\t\t\tconst response = await fetch(apiUrl, {\n\t\t\t\tmethod: 'GET',\n\t\t\t\tcredentials: 'include',\n\t\t\t\theaders: { Accept: 'application/json' },\n\t\t\t})\n\n\t\t\tconst payload = await response.json().catch(() => null)\n\n\t\t\tif (!response.ok || payload?.ok === false) {\n\t\t\t\tthrow new Error(payload?.error || `Authentication failed (${response.status})`)\n\t\t\t}\n\n\t\t\tif (!payload?.redirectTo) {\n\t\t\t\tthrow new Error('Missing redirect target from authentication response.')\n\t\t\t}\n\n\t\t\twindow.location.href = new URL(payload.redirectTo, previewOrigin).toString()\n\t\t} catch (err) {\n\t\t\tsetError(err instanceof Error ? err.message : 'Failed to authenticate preview mode.')\n\t\t\tsetIsAuthenticating(false)\n\t\t}\n\t}, [client, currentUser?.id, previewAuthApi, previewOrigin, redirectUrl])\n\n\treturn { redirectUrl, fullRedirectUrl, isAuthenticating, error, handleAuthenticate }\n}\n","import React from 'react'\nimport { Button, Card, Container, Flex, Heading, Stack, Text } from '@sanity/ui'\nimport { EyeOpenIcon } from '@sanity/icons'\nimport { usePreviewAuth } from '../hooks/usePreviewAuth'\n\nexport type PreviewAuthPageProps = {\n\tpreviewOrigin: string\n\tpreviewAuthApi: string\n}\n\nexport function PreviewAuthPage({ previewOrigin, previewAuthApi }: PreviewAuthPageProps) {\n\tconst { redirectUrl, fullRedirectUrl, isAuthenticating, error, handleAuthenticate } =\n\t\tusePreviewAuth({ previewOrigin, previewAuthApi })\n\n\tif (!redirectUrl) {\n\t\treturn (\n\t\t\t<Card height='fill' overflow='auto'>\n\t\t\t\t<Container width={1} padding={5}>\n\t\t\t\t\t<Flex justify='center' align='center' style={{ minHeight: '60vh' }}>\n\t\t\t\t\t\t<Card padding={5} radius={3} shadow={1} style={{ maxWidth: 480, width: '100%' }}>\n\t\t\t\t\t\t\t<Stack space={4} style={{ textAlign: 'center' }}>\n\t\t\t\t\t\t\t\t<Heading size={4}>404</Heading>\n\t\t\t\t\t\t\t\t<Text muted size={2}>\n\t\t\t\t\t\t\t\t\tPreview authentication requires a redirect URL.\n\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t</Flex>\n\t\t\t\t</Container>\n\t\t\t</Card>\n\t\t)\n\t}\n\n\treturn (\n\t\t<Card height='fill' overflow='auto'>\n\t\t\t<Container width={1} padding={5}>\n\t\t\t\t<Flex justify='center' align='center' style={{ minHeight: '60vh' }}>\n\t\t\t\t\t<Card padding={5} radius={3} shadow={1} style={{ maxWidth: 480, width: '100%' }}>\n\t\t\t\t\t\t<Stack space={5}>\n\t\t\t\t\t\t\t<Flex justify='center'>\n\t\t\t\t\t\t\t\t<Card padding={3} radius='full' tone='primary'>\n\t\t\t\t\t\t\t\t\t<Text size={4}>\n\t\t\t\t\t\t\t\t\t\t<EyeOpenIcon />\n\t\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t</Flex>\n\n\t\t\t\t\t\t\t<Stack space={3} style={{ textAlign: 'center' }}>\n\t\t\t\t\t\t\t\t<Heading size={2}>Preview Mode Authentication</Heading>\n\t\t\t\t\t\t\t\t<Text muted size={2}>\n\t\t\t\t\t\t\t\t\tAuthenticate to access the preview environment. This generates a secure token\n\t\t\t\t\t\t\t\t\tvalid for 3 months.\n\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t</Stack>\n\n\t\t\t\t\t\t\t{fullRedirectUrl && (\n\t\t\t\t\t\t\t\t<Card padding={3} radius={2} tone='positive'>\n\t\t\t\t\t\t\t\t\t<Stack space={3}>\n\t\t\t\t\t\t\t\t\t\t<Text size={0} muted weight='medium'>\n\t\t\t\t\t\t\t\t\t\t\tRedirecting to:\n\t\t\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t\t\t\t<Text size={1} style={{ wordBreak: 'break-all' }}>\n\t\t\t\t\t\t\t\t\t\t\t{fullRedirectUrl}\n\t\t\t\t\t\t\t\t\t\t</Text>\n\t\t\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t{error && (\n\t\t\t\t\t\t\t\t<Card padding={3} radius={2} tone='critical'>\n\t\t\t\t\t\t\t\t\t<Text size={1}>{error}</Text>\n\t\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\tfontSize={2}\n\t\t\t\t\t\t\t\tpadding={4}\n\t\t\t\t\t\t\t\ttone='primary'\n\t\t\t\t\t\t\t\tloading={isAuthenticating}\n\t\t\t\t\t\t\t\ttext={isAuthenticating ? 'Authenticating…' : 'Authenticate Preview Mode'}\n\t\t\t\t\t\t\t\ticon={EyeOpenIcon}\n\t\t\t\t\t\t\t\tonClick={handleAuthenticate}\n\t\t\t\t\t\t\t\tdisabled={isAuthenticating || !previewOrigin}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t</Card>\n\t\t\t\t</Flex>\n\t\t\t</Container>\n\t\t</Card>\n\t)\n}\n","import { defineField, defineType } from 'sanity';\nimport { PREVIEW_AUTH_SECRET_TYPE } from 'sanity-plugin-preview-auth-validate';\n\n/**\n * System document type for long-lived preview auth secrets.\n * Add this to your Sanity schema types array, or use `previewAuthPlugin`\n * which registers it automatically.\n * @public\n */\nexport const previewAuthSecret = defineType({\n name: PREVIEW_AUTH_SECRET_TYPE,\n type: 'document',\n title: 'Preview Auth Secret',\n fields: [\n defineField({ name: 'secret', type: 'string', title: 'Secret' }),\n defineField({ name: 'source', type: 'string', title: 'Source' }),\n defineField({ name: 'studioUrl', type: 'string', title: 'Studio URL' }),\n defineField({ name: 'userId', type: 'string', title: 'User ID' }),\n defineField({ name: 'expiresAt', type: 'datetime', title: 'Expires At' })\n ]\n});\n","import { definePlugin } from 'sanity';\nimport { EyeOpenIcon } from '@sanity/icons';\nimport { PreviewAuthPage } from './components/PreviewAuthPage';\nimport { previewAuthSecret } from './schema/previewAuthSecret';\n\n/** @public */\nexport type PreviewAuthPluginOptions = {\n /** The origin of your preview site, e.g. https://preview.mysite.com */\n previewOrigin: string;\n /** The path to your draft-mode enable API, e.g. /api/draft-mode/enable */\n previewAuthApi: string;\n};\n\nconst TOOL_NAME = 'preview-auth';\n\n/**\n * Sanity Studio plugin for long-lived cross-origin preview authentication.\n *\n * Registers:\n * - A hidden `preview-auth` tool reachable via `/preview-auth?redirect=…`\n * - The `sanity.previewAuthSecret` schema type for long-lived secrets\n * @public\n */\nexport const previewAuthPlugin = definePlugin<PreviewAuthPluginOptions>((options) => ({\n name: TOOL_NAME,\n schema: {\n types: [previewAuthSecret]\n },\n tools: [\n {\n name: TOOL_NAME,\n title: 'Preview Auth',\n icon: EyeOpenIcon,\n component: () => PreviewAuthPage(options)\n }\n ],\n studio: {\n components: {\n toolMenu: (props) => {\n const filteredTools = props.tools.filter((tool) => tool.name !== TOOL_NAME);\n\n return props.renderDefault({ ...props, tools: filteredTools });\n }\n }\n }\n}));\n","import React, { useState } from 'react'\nimport { useClient, useCurrentUser, DEFAULT_STUDIO_CLIENT_OPTIONS } from 'sanity'\nimport type { DocumentActionComponent, DocumentActionProps } from 'sanity'\nimport { EyeOpenIcon } from '@sanity/icons'\nimport { createPreviewSecret } from 'sanity-plugin-preview-auth-validate'\n\n/** @public */\nexport type OpenPreviewActionOptions = {\n\tpreviewOrigin: string\n\t/** Slug field name on route documents (default: 'slug') */\n\tslugField?: string\n}\n\nfunction createAction(options: OpenPreviewActionOptions): DocumentActionComponent {\n\tconst { previewOrigin, slugField = 'slug' } = options\n\n\tconst OpenPreviewAction = (props: DocumentActionProps) => {\n\t\tconst client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)\n\t\tconst currentUser = useCurrentUser()\n\t\tconst [isGenerating, setIsGenerating] = useState(false)\n\n\t\tconst slug = (props.draft ?? props.published)?.[slugField] as\n\t\t\t| { current?: string }\n\t\t\t| string\n\t\t\t| undefined\n\n\t\tconst slugValue = typeof slug === 'string' ? slug : slug?.current\n\n\t\tif (!slugValue) {\n\t\t\treturn null\n\t\t}\n\n\t\treturn {\n\t\t\tlabel: isGenerating ? 'Opening…' : 'Open Preview',\n\t\t\ticon: EyeOpenIcon,\n\t\t\tdisabled: isGenerating,\n\t\t\tonHandle: async () => {\n\t\t\t\tsetIsGenerating(true)\n\n\t\t\t\ttry {\n\t\t\t\t\tconst { secret } = await createPreviewSecret({\n\t\t\t\t\t\tclient,\n\t\t\t\t\t\tsource: 'sanity/open-preview-action',\n\t\t\t\t\t\tstudioUrl: window.location.href,\n\t\t\t\t\t\tuserId: currentUser?.id,\n\t\t\t\t\t})\n\n\t\t\t\t\tconst previewPath = slugValue.startsWith('/') ? slugValue : `/${slugValue}`\n\t\t\t\t\tconst url = new URL(`/preview${previewPath}`, previewOrigin)\n\n\t\t\t\t\turl.searchParams.set('sanity-preview-secret', secret)\n\n\t\t\t\t\twindow.open(url.toString(), '_blank', 'noopener,noreferrer')\n\t\t\t\t} finally {\n\t\t\t\t\tsetIsGenerating(false)\n\t\t\t\t\tprops.onComplete()\n\t\t\t\t}\n\t\t\t},\n\t\t}\n\t}\n\n\tOpenPreviewAction.action = 'openPreview'\n\n\treturn OpenPreviewAction as DocumentActionComponent\n}\n\n/** @public */\nexport function openPreviewAction(options: OpenPreviewActionOptions): DocumentActionComponent {\n\treturn createAction(options)\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sanity-plugin-preview-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sanity Studio plugin for long-lived cross-origin preview authentication",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sanity",
|
|
7
|
+
"sanity-plugin",
|
|
8
|
+
"preview",
|
|
9
|
+
"authentication",
|
|
10
|
+
"draft-mode"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/ameenaburayya/sanity-plugin-preview-auth",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/ameenaburayya/sanity-plugin-preview-auth.git",
|
|
16
|
+
"directory": "packages/plugin"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"source": "./src/index.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"require": "./dist/index.cjs",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"browserslist": "extends @sanity/browserslist-config",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
|
|
43
|
+
"watch": "pkg-utils watch --strict",
|
|
44
|
+
"link-watch": "plugin-kit link-watch",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"prepublishOnly": "pnpm run build"
|
|
47
|
+
},
|
|
48
|
+
"sanityPlugin": {
|
|
49
|
+
"verifyPackage": {
|
|
50
|
+
"sanityV2Json": false,
|
|
51
|
+
"eslintImports": false
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"sanity-plugin-preview-auth-validate": "workspace:*"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@sanity/icons": ">=3.0.0",
|
|
59
|
+
"@sanity/ui": ">=2.0.0",
|
|
60
|
+
"react": ">=18.0.0",
|
|
61
|
+
"sanity": ">=3.0.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@sanity/browserslist-config": "^1.0.5",
|
|
65
|
+
"@sanity/icons": "^3.0.0",
|
|
66
|
+
"@sanity/pkg-utils": "^6.0.0",
|
|
67
|
+
"@sanity/plugin-kit": "^4.0.0",
|
|
68
|
+
"@sanity/ui": "^2.0.0",
|
|
69
|
+
"@types/react": "^18.0.0",
|
|
70
|
+
"react": "^18.0.0",
|
|
71
|
+
"sanity": "^5.0.0",
|
|
72
|
+
"typescript": "^5.0.0"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { useClient, useCurrentUser, DEFAULT_STUDIO_CLIENT_OPTIONS } from 'sanity'
|
|
3
|
+
import type { DocumentActionComponent, DocumentActionProps } from 'sanity'
|
|
4
|
+
import { EyeOpenIcon } from '@sanity/icons'
|
|
5
|
+
import { createPreviewSecret } from 'sanity-plugin-preview-auth-validate'
|
|
6
|
+
|
|
7
|
+
/** @public */
|
|
8
|
+
export type OpenPreviewActionOptions = {
|
|
9
|
+
previewOrigin: string
|
|
10
|
+
/** Slug field name on route documents (default: 'slug') */
|
|
11
|
+
slugField?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createAction(options: OpenPreviewActionOptions): DocumentActionComponent {
|
|
15
|
+
const { previewOrigin, slugField = 'slug' } = options
|
|
16
|
+
|
|
17
|
+
const OpenPreviewAction = (props: DocumentActionProps) => {
|
|
18
|
+
const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
|
|
19
|
+
const currentUser = useCurrentUser()
|
|
20
|
+
const [isGenerating, setIsGenerating] = useState(false)
|
|
21
|
+
|
|
22
|
+
const slug = (props.draft ?? props.published)?.[slugField] as
|
|
23
|
+
| { current?: string }
|
|
24
|
+
| string
|
|
25
|
+
| undefined
|
|
26
|
+
|
|
27
|
+
const slugValue = typeof slug === 'string' ? slug : slug?.current
|
|
28
|
+
|
|
29
|
+
if (!slugValue) {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
label: isGenerating ? 'Opening…' : 'Open Preview',
|
|
35
|
+
icon: EyeOpenIcon,
|
|
36
|
+
disabled: isGenerating,
|
|
37
|
+
onHandle: async () => {
|
|
38
|
+
setIsGenerating(true)
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const { secret } = await createPreviewSecret({
|
|
42
|
+
client,
|
|
43
|
+
source: 'sanity/open-preview-action',
|
|
44
|
+
studioUrl: window.location.href,
|
|
45
|
+
userId: currentUser?.id,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const previewPath = slugValue.startsWith('/') ? slugValue : `/${slugValue}`
|
|
49
|
+
const url = new URL(`/preview${previewPath}`, previewOrigin)
|
|
50
|
+
|
|
51
|
+
url.searchParams.set('sanity-preview-secret', secret)
|
|
52
|
+
|
|
53
|
+
window.open(url.toString(), '_blank', 'noopener,noreferrer')
|
|
54
|
+
} finally {
|
|
55
|
+
setIsGenerating(false)
|
|
56
|
+
props.onComplete()
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
OpenPreviewAction.action = 'openPreview'
|
|
63
|
+
|
|
64
|
+
return OpenPreviewAction as DocumentActionComponent
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @public */
|
|
68
|
+
export function openPreviewAction(options: OpenPreviewActionOptions): DocumentActionComponent {
|
|
69
|
+
return createAction(options)
|
|
70
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Button, Card, Container, Flex, Heading, Stack, Text } from '@sanity/ui'
|
|
3
|
+
import { EyeOpenIcon } from '@sanity/icons'
|
|
4
|
+
import { usePreviewAuth } from '../hooks/usePreviewAuth'
|
|
5
|
+
|
|
6
|
+
export type PreviewAuthPageProps = {
|
|
7
|
+
previewOrigin: string
|
|
8
|
+
previewAuthApi: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function PreviewAuthPage({ previewOrigin, previewAuthApi }: PreviewAuthPageProps) {
|
|
12
|
+
const { redirectUrl, fullRedirectUrl, isAuthenticating, error, handleAuthenticate } =
|
|
13
|
+
usePreviewAuth({ previewOrigin, previewAuthApi })
|
|
14
|
+
|
|
15
|
+
if (!redirectUrl) {
|
|
16
|
+
return (
|
|
17
|
+
<Card height='fill' overflow='auto'>
|
|
18
|
+
<Container width={1} padding={5}>
|
|
19
|
+
<Flex justify='center' align='center' style={{ minHeight: '60vh' }}>
|
|
20
|
+
<Card padding={5} radius={3} shadow={1} style={{ maxWidth: 480, width: '100%' }}>
|
|
21
|
+
<Stack space={4} style={{ textAlign: 'center' }}>
|
|
22
|
+
<Heading size={4}>404</Heading>
|
|
23
|
+
<Text muted size={2}>
|
|
24
|
+
Preview authentication requires a redirect URL.
|
|
25
|
+
</Text>
|
|
26
|
+
</Stack>
|
|
27
|
+
</Card>
|
|
28
|
+
</Flex>
|
|
29
|
+
</Container>
|
|
30
|
+
</Card>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Card height='fill' overflow='auto'>
|
|
36
|
+
<Container width={1} padding={5}>
|
|
37
|
+
<Flex justify='center' align='center' style={{ minHeight: '60vh' }}>
|
|
38
|
+
<Card padding={5} radius={3} shadow={1} style={{ maxWidth: 480, width: '100%' }}>
|
|
39
|
+
<Stack space={5}>
|
|
40
|
+
<Flex justify='center'>
|
|
41
|
+
<Card padding={3} radius='full' tone='primary'>
|
|
42
|
+
<Text size={4}>
|
|
43
|
+
<EyeOpenIcon />
|
|
44
|
+
</Text>
|
|
45
|
+
</Card>
|
|
46
|
+
</Flex>
|
|
47
|
+
|
|
48
|
+
<Stack space={3} style={{ textAlign: 'center' }}>
|
|
49
|
+
<Heading size={2}>Preview Mode Authentication</Heading>
|
|
50
|
+
<Text muted size={2}>
|
|
51
|
+
Authenticate to access the preview environment. This generates a secure token
|
|
52
|
+
valid for 3 months.
|
|
53
|
+
</Text>
|
|
54
|
+
</Stack>
|
|
55
|
+
|
|
56
|
+
{fullRedirectUrl && (
|
|
57
|
+
<Card padding={3} radius={2} tone='positive'>
|
|
58
|
+
<Stack space={3}>
|
|
59
|
+
<Text size={0} muted weight='medium'>
|
|
60
|
+
Redirecting to:
|
|
61
|
+
</Text>
|
|
62
|
+
<Text size={1} style={{ wordBreak: 'break-all' }}>
|
|
63
|
+
{fullRedirectUrl}
|
|
64
|
+
</Text>
|
|
65
|
+
</Stack>
|
|
66
|
+
</Card>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{error && (
|
|
70
|
+
<Card padding={3} radius={2} tone='critical'>
|
|
71
|
+
<Text size={1}>{error}</Text>
|
|
72
|
+
</Card>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
<Button
|
|
76
|
+
fontSize={2}
|
|
77
|
+
padding={4}
|
|
78
|
+
tone='primary'
|
|
79
|
+
loading={isAuthenticating}
|
|
80
|
+
text={isAuthenticating ? 'Authenticating…' : 'Authenticate Preview Mode'}
|
|
81
|
+
icon={EyeOpenIcon}
|
|
82
|
+
onClick={handleAuthenticate}
|
|
83
|
+
disabled={isAuthenticating || !previewOrigin}
|
|
84
|
+
/>
|
|
85
|
+
</Stack>
|
|
86
|
+
</Card>
|
|
87
|
+
</Flex>
|
|
88
|
+
</Container>
|
|
89
|
+
</Card>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo } from 'react'
|
|
2
|
+
import { useClient, useCurrentUser, DEFAULT_STUDIO_CLIENT_OPTIONS } from 'sanity'
|
|
3
|
+
import { createPreviewSecret } from 'sanity-plugin-preview-auth-validate'
|
|
4
|
+
|
|
5
|
+
/** @public */
|
|
6
|
+
export type UsePreviewAuthOptions = {
|
|
7
|
+
previewOrigin: string
|
|
8
|
+
previewAuthApi: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** @public */
|
|
12
|
+
export type UsePreviewAuthResult = {
|
|
13
|
+
redirectUrl: string | null
|
|
14
|
+
fullRedirectUrl: string | null
|
|
15
|
+
isAuthenticating: boolean
|
|
16
|
+
error: string | null
|
|
17
|
+
handleAuthenticate: () => Promise<void>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @public */
|
|
21
|
+
export function usePreviewAuth({
|
|
22
|
+
previewOrigin,
|
|
23
|
+
previewAuthApi,
|
|
24
|
+
}: UsePreviewAuthOptions): UsePreviewAuthResult {
|
|
25
|
+
const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
|
|
26
|
+
const currentUser = useCurrentUser()
|
|
27
|
+
|
|
28
|
+
const [isAuthenticating, setIsAuthenticating] = useState(false)
|
|
29
|
+
const [error, setError] = useState<string | null>(null)
|
|
30
|
+
|
|
31
|
+
const redirectUrl = useMemo(() => {
|
|
32
|
+
if (typeof window === 'undefined') {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new URLSearchParams(window.location.search).get('redirect')
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
const fullRedirectUrl = useMemo(() => {
|
|
40
|
+
if (!redirectUrl || !previewOrigin) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
return new URL(redirectUrl, previewOrigin).toString()
|
|
46
|
+
} catch {
|
|
47
|
+
return `${previewOrigin}${redirectUrl}`
|
|
48
|
+
}
|
|
49
|
+
}, [redirectUrl, previewOrigin])
|
|
50
|
+
|
|
51
|
+
const handleAuthenticate = useCallback(async () => {
|
|
52
|
+
if (!previewOrigin) {
|
|
53
|
+
setError('No preview URL configured for this workspace.')
|
|
54
|
+
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setIsAuthenticating(true)
|
|
59
|
+
setError(null)
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const { secret } = await createPreviewSecret({
|
|
63
|
+
client,
|
|
64
|
+
source: 'sanity/preview-auth',
|
|
65
|
+
studioUrl: window.location.href,
|
|
66
|
+
userId: currentUser?.id,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const apiUrl = new URL(previewAuthApi, previewOrigin)
|
|
70
|
+
const redirectPath = redirectUrl?.startsWith('/preview')
|
|
71
|
+
? redirectUrl.replace(/^\/preview/, '') || '/'
|
|
72
|
+
: redirectUrl || '/'
|
|
73
|
+
|
|
74
|
+
apiUrl.searchParams.set('sanity-preview-secret', secret)
|
|
75
|
+
apiUrl.searchParams.set('sanity-preview-pathname', redirectPath)
|
|
76
|
+
apiUrl.searchParams.set('response', 'json')
|
|
77
|
+
|
|
78
|
+
const response = await fetch(apiUrl, {
|
|
79
|
+
method: 'GET',
|
|
80
|
+
credentials: 'include',
|
|
81
|
+
headers: { Accept: 'application/json' },
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const payload = await response.json().catch(() => null)
|
|
85
|
+
|
|
86
|
+
if (!response.ok || payload?.ok === false) {
|
|
87
|
+
throw new Error(payload?.error || `Authentication failed (${response.status})`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!payload?.redirectTo) {
|
|
91
|
+
throw new Error('Missing redirect target from authentication response.')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
window.location.href = new URL(payload.redirectTo, previewOrigin).toString()
|
|
95
|
+
} catch (err) {
|
|
96
|
+
setError(err instanceof Error ? err.message : 'Failed to authenticate preview mode.')
|
|
97
|
+
setIsAuthenticating(false)
|
|
98
|
+
}
|
|
99
|
+
}, [client, currentUser?.id, previewAuthApi, previewOrigin, redirectUrl])
|
|
100
|
+
|
|
101
|
+
return { redirectUrl, fullRedirectUrl, isAuthenticating, error, handleAuthenticate }
|
|
102
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { previewAuthPlugin } from './plugin';
|
|
2
|
+
export type { PreviewAuthPluginOptions } from './plugin';
|
|
3
|
+
export { previewAuthSecret } from './schema/previewAuthSecret';
|
|
4
|
+
export { usePreviewAuth } from './hooks/usePreviewAuth';
|
|
5
|
+
export type { UsePreviewAuthOptions, UsePreviewAuthResult } from './hooks/usePreviewAuth';
|
|
6
|
+
export { openPreviewAction } from './actions/openPreviewAction';
|
|
7
|
+
export type { OpenPreviewActionOptions } from './actions/openPreviewAction';
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { definePlugin } from 'sanity';
|
|
2
|
+
import { EyeOpenIcon } from '@sanity/icons';
|
|
3
|
+
import { PreviewAuthPage } from './components/PreviewAuthPage';
|
|
4
|
+
import { previewAuthSecret } from './schema/previewAuthSecret';
|
|
5
|
+
|
|
6
|
+
/** @public */
|
|
7
|
+
export type PreviewAuthPluginOptions = {
|
|
8
|
+
/** The origin of your preview site, e.g. https://preview.mysite.com */
|
|
9
|
+
previewOrigin: string;
|
|
10
|
+
/** The path to your draft-mode enable API, e.g. /api/draft-mode/enable */
|
|
11
|
+
previewAuthApi: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const TOOL_NAME = 'preview-auth';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sanity Studio plugin for long-lived cross-origin preview authentication.
|
|
18
|
+
*
|
|
19
|
+
* Registers:
|
|
20
|
+
* - A hidden `preview-auth` tool reachable via `/preview-auth?redirect=…`
|
|
21
|
+
* - The `sanity.previewAuthSecret` schema type for long-lived secrets
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export const previewAuthPlugin = definePlugin<PreviewAuthPluginOptions>((options) => ({
|
|
25
|
+
name: TOOL_NAME,
|
|
26
|
+
schema: {
|
|
27
|
+
types: [previewAuthSecret]
|
|
28
|
+
},
|
|
29
|
+
tools: [
|
|
30
|
+
{
|
|
31
|
+
name: TOOL_NAME,
|
|
32
|
+
title: 'Preview Auth',
|
|
33
|
+
icon: EyeOpenIcon,
|
|
34
|
+
component: () => PreviewAuthPage(options)
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
studio: {
|
|
38
|
+
components: {
|
|
39
|
+
toolMenu: (props) => {
|
|
40
|
+
const filteredTools = props.tools.filter((tool) => tool.name !== TOOL_NAME);
|
|
41
|
+
|
|
42
|
+
return props.renderDefault({ ...props, tools: filteredTools });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineField, defineType } from 'sanity';
|
|
2
|
+
import { PREVIEW_AUTH_SECRET_TYPE } from 'sanity-plugin-preview-auth-validate';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* System document type for long-lived preview auth secrets.
|
|
6
|
+
* Add this to your Sanity schema types array, or use `previewAuthPlugin`
|
|
7
|
+
* which registers it automatically.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export const previewAuthSecret = defineType({
|
|
11
|
+
name: PREVIEW_AUTH_SECRET_TYPE,
|
|
12
|
+
type: 'document',
|
|
13
|
+
title: 'Preview Auth Secret',
|
|
14
|
+
fields: [
|
|
15
|
+
defineField({ name: 'secret', type: 'string', title: 'Secret' }),
|
|
16
|
+
defineField({ name: 'source', type: 'string', title: 'Source' }),
|
|
17
|
+
defineField({ name: 'studioUrl', type: 'string', title: 'Studio URL' }),
|
|
18
|
+
defineField({ name: 'userId', type: 'string', title: 'User ID' }),
|
|
19
|
+
defineField({ name: 'expiresAt', type: 'datetime', title: 'Expires At' })
|
|
20
|
+
]
|
|
21
|
+
});
|