@tetherto/dev-websites-core 0.3.3 → 0.3.4
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/README.md +72 -0
- package/dist/cms/google-oauth.d.mts +20 -0
- package/dist/cms/google-oauth.d.mts.map +1 -0
- package/dist/cms/google-oauth.mjs +130 -0
- package/dist/cms/google-oauth.mjs.map +1 -0
- package/dist/cms/index.d.mts +2 -2
- package/dist/cms/index.mjs +2 -2
- package/dist/cms.mjs +136 -5
- package/dist/cms.mjs.map +1 -1
- package/dist/index.d.mts +11 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -3
- package/dist/index5.d.mts +2 -2
- package/dist/seo/index.mjs +2 -1
- package/dist/seo.mjs +2 -13
- package/dist/seo.mjs.map +1 -1
- package/dist/site-url.mjs +16 -0
- package/dist/site-url.mjs.map +1 -0
- package/dist/ui/components/Button/Button.d.mts +1 -1
- package/dist/ui/components/GoogleOAuthButton/GoogleOAuthButton.d.mts +5 -0
- package/dist/ui/components/GoogleOAuthButton/GoogleOAuthButton.d.mts.map +1 -0
- package/dist/ui/components/GoogleOAuthButton/GoogleOAuthButton.mjs +72 -0
- package/dist/ui/components/GoogleOAuthButton/GoogleOAuthButton.mjs.map +1 -0
- package/dist/ui/components/GoogleOAuthButton/googleOAuthStartPath.d.mts +6 -0
- package/dist/ui/components/GoogleOAuthButton/googleOAuthStartPath.d.mts.map +1 -0
- package/dist/ui/components/GoogleOAuthButton/googleOAuthStartPath.mjs +7 -0
- package/dist/ui/components/GoogleOAuthButton/googleOAuthStartPath.mjs.map +1 -0
- package/dist/ui/components/GoogleOAuthButton/index.d.mts +2 -0
- package/dist/ui/components/GoogleOAuthButton/index.mjs +2 -0
- package/dist/ui/index.d.mts +3 -1
- package/dist/ui/index.mjs +1 -0
- package/package.json +18 -2
- package/src/ui/components/GoogleOAuthButton/GoogleOAuthButton.tsx +69 -0
- package/src/ui/components/GoogleOAuthButton/googleOAuthStartPath.ts +2 -0
- package/src/ui/components/GoogleOAuthButton/index.ts +1 -0
package/README.md
CHANGED
|
@@ -5,11 +5,13 @@ Shared CMS, SEO, i18n, utils, UI, and analytics primitives for WDK / QVAC / PEAR
|
|
|
5
5
|
| Import | Contents |
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| `@tetherto/dev-websites-core/cms` | Payload `buildCmsConfig`, stock collections, access, Lexical, media I/O |
|
|
8
|
+
| `@tetherto/dev-websites-core/cms/google-oauth` | Google admin login handlers (`google-auth-library` peer) |
|
|
8
9
|
| `@tetherto/dev-websites-core/seo` | `createMetadataFactory`, `getSiteUrl`, `getOgBackgroundDataUrl` |
|
|
9
10
|
| `@tetherto/dev-websites-core/i18n` | `Languages` / `LanguageLabel` |
|
|
10
11
|
| `@tetherto/dev-websites-core/utils` | `createLogger`, `toPlainValue`, `stripShikiPreBackground` |
|
|
11
12
|
| `@tetherto/dev-websites-core/analytics` | `GtmLoader`, `createTrackedLink` (optional `@next/third-parties` peer) |
|
|
12
13
|
| `@tetherto/dev-websites-core/ui` | UI primitives (`Button`, `Dialog`, `Pagination`, `Tooltip`, …) + `cn` |
|
|
14
|
+
| `@tetherto/dev-websites-core/ui/google-oauth` | Payload `GoogleOAuthButton` (Tailwind classes; source `dist` from Payload CSS) |
|
|
13
15
|
| `@tetherto/dev-websites-core/ui/a11y` | `focusRingClassName`, `VisuallyHidden` |
|
|
14
16
|
| `@tetherto/dev-websites-core/ui/carousel` | `Carousel` (+ optional `embla-carousel-react` peer) |
|
|
15
17
|
| `@tetherto/dev-websites-core/ui/theme.css` | One-shot: tokens + Tailwind v4 `@theme` preset |
|
|
@@ -51,6 +53,71 @@ rm -rf node_modules/@tetherto/dev-websites-core && npm install @tetherto/dev-web
|
|
|
51
53
|
Do **not** import `@tetherto/dev-websites-core/cms` from Client Components — it includes Node-only
|
|
52
54
|
modules (`fs`, Payload, S3). Prefer domain subpaths over the root barrel.
|
|
53
55
|
|
|
56
|
+
### Google admin login
|
|
57
|
+
|
|
58
|
+
Stock `users` includes `googleId` / `name` / `picture`. Sites add Next.js routes and
|
|
59
|
+
Payload `afterLogin` (optional `google-auth-library` peer):
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// src/payload.config.ts
|
|
63
|
+
import { GOOGLE_OAUTH_AFTER_LOGIN } from '@tetherto/dev-websites-core/cms/google-oauth'
|
|
64
|
+
|
|
65
|
+
admin: {
|
|
66
|
+
components: { afterLogin: [...GOOGLE_OAUTH_AFTER_LOGIN] },
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
// src/app/api/auth/google/route.ts
|
|
72
|
+
import { handleGoogleOAuthStart } from '@tetherto/dev-websites-core/cms/google-oauth'
|
|
73
|
+
|
|
74
|
+
export function GET() {
|
|
75
|
+
return handleGoogleOAuthStart({
|
|
76
|
+
clientId: process.env.GOOGLE_OAUTH_CLIENT_ID ?? '',
|
|
77
|
+
clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET ?? '',
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// src/app/api/auth/google/callback/route.ts
|
|
84
|
+
import { handleGoogleOAuthCallback } from '@tetherto/dev-websites-core/cms/google-oauth'
|
|
85
|
+
import config from '@payload-config'
|
|
86
|
+
|
|
87
|
+
import type { NextRequest } from 'next/server'
|
|
88
|
+
|
|
89
|
+
export function GET(request: NextRequest) {
|
|
90
|
+
return handleGoogleOAuthCallback(request, {
|
|
91
|
+
config,
|
|
92
|
+
clientId: process.env.GOOGLE_OAUTH_CLIENT_ID ?? '',
|
|
93
|
+
clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET ?? '',
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```css
|
|
99
|
+
/* src/app/(payload)/custom.css — utilities only; do not import Tailwind preflight */
|
|
100
|
+
@import 'tailwindcss/theme.css' layer(theme) source(none);
|
|
101
|
+
@import 'tailwindcss/utilities.css' layer(utilities) source(none);
|
|
102
|
+
@source '../../../node_modules/@tetherto/dev-websites-core/src/ui/components/GoogleOAuthButton';
|
|
103
|
+
|
|
104
|
+
.login__form > .form-submit .btn {
|
|
105
|
+
margin-bottom: 0;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Apps using Next.js should also transpile the package so the Payload button shares the app React instance:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// next.config.ts
|
|
113
|
+
transpilePackages: ['@tetherto/dev-websites-core']
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The app passes `clientId` and `clientSecret` into both handlers (usually from its own
|
|
117
|
+
`GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`). Redirect URI:
|
|
118
|
+
`{NEXT_PUBLIC_SITE_URL}/api/auth/google/callback`. Existing `users` emails are
|
|
119
|
+
required — Google login does not create accounts.
|
|
120
|
+
|
|
54
121
|
### Analytics
|
|
55
122
|
|
|
56
123
|
```ts
|
|
@@ -142,6 +209,11 @@ Changelog stock ships shared fields only; site-specific body / GitHub /
|
|
|
142
209
|
`overrides.hooks` (site-owned, e.g. `src/lib/cache`) — hook arrays are **appended**
|
|
143
210
|
to stock hooks, not replaced.
|
|
144
211
|
|
|
212
|
+
Draft collections enable Payload `schedulePublish`. `buildCmsConfig` starts
|
|
213
|
+
`jobs.autoRun` every minute so scheduled publishes run in a long-lived Node
|
|
214
|
+
process (`next start` / Docker). Override or disable via `payload.jobs`. This
|
|
215
|
+
is not for serverless hosts.
|
|
216
|
+
|
|
145
217
|
### Peer dependencies
|
|
146
218
|
|
|
147
219
|
CMS / Next (required for `/cms` consumers):
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Config, SanitizedConfig } from "payload";
|
|
2
|
+
import { NextRequest, NextResponse } from "next/server.js";
|
|
3
|
+
//#region src/cms/auth/google-oauth.d.ts
|
|
4
|
+
/** Payload `admin.components.afterLogin` path for the shared Google button. */
|
|
5
|
+
declare const GOOGLE_OAUTH_AFTER_LOGIN: readonly ["@tetherto/dev-websites-core/ui/google-oauth#GoogleOAuthButton"];
|
|
6
|
+
type GoogleOAuthClientConfig = {
|
|
7
|
+
clientId: string;
|
|
8
|
+
clientSecret: string;
|
|
9
|
+
};
|
|
10
|
+
type HandleGoogleOAuthCallbackOptions = GoogleOAuthClientConfig & {
|
|
11
|
+
/** Site Payload config default export (`import config from '@payload-config'`). */
|
|
12
|
+
config: Config | SanitizedConfig | Promise<Config | SanitizedConfig>;
|
|
13
|
+
};
|
|
14
|
+
/** GET `/api/auth/google` — start the Google OAuth redirect. */
|
|
15
|
+
declare function handleGoogleOAuthStart(clientConfig: GoogleOAuthClientConfig): Promise<NextResponse<unknown>>;
|
|
16
|
+
/** GET `/api/auth/google/callback` — exchange the code and set the Payload session cookie. */
|
|
17
|
+
declare function handleGoogleOAuthCallback(request: NextRequest, { config, ...clientConfig }: HandleGoogleOAuthCallbackOptions): Promise<NextResponse<unknown>>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { GOOGLE_OAUTH_AFTER_LOGIN, GoogleOAuthClientConfig, HandleGoogleOAuthCallbackOptions, handleGoogleOAuthCallback, handleGoogleOAuthStart };
|
|
20
|
+
//# sourceMappingURL=google-oauth.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google-oauth.d.mts","names":[],"sources":["../../src/cms/auth/google-oauth.ts"],"mappings":";;;;cAiBa;KAMD;EACV;EACA;;KAGU,mCAAmC;;EAE7C,QAAQ,SAAS,kBAAkB,QAAQ,SAAS;;;iBAqBhC,uBAAuB,cAAc,0BAAuB,QAAA;;iBAoB5D,0BACpB,SAAS,eACP,WAAW,gBAAgB,mCAAgC,QAAA"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { t as createLogger } from "../logger.mjs";
|
|
2
|
+
import { t as getSiteUrl } from "../site-url.mjs";
|
|
3
|
+
import { getFieldsToSign, getPayload, jwtSign } from "payload";
|
|
4
|
+
import { OAuth2Client } from "google-auth-library";
|
|
5
|
+
import { NextResponse } from "next/server.js";
|
|
6
|
+
import { addSessionToUser, generatePayloadCookie } from "payload/shared";
|
|
7
|
+
//#region src/cms/auth/google-oauth.ts
|
|
8
|
+
const log = createLogger("googleOAuth");
|
|
9
|
+
const GOOGLE_OAUTH_CALLBACK_PATH = "/api/auth/google/callback";
|
|
10
|
+
const GOOGLE_OAUTH_LOGIN_PATH = "/admin/login";
|
|
11
|
+
const GOOGLE_OAUTH_ADMIN_PATH = "/admin";
|
|
12
|
+
/** Payload `admin.components.afterLogin` path for the shared Google button. */
|
|
13
|
+
const GOOGLE_OAUTH_AFTER_LOGIN = ["@tetherto/dev-websites-core/ui/google-oauth#GoogleOAuthButton"];
|
|
14
|
+
const GOOGLE_OAUTH_SCOPES = [
|
|
15
|
+
"openid",
|
|
16
|
+
"email",
|
|
17
|
+
"profile"
|
|
18
|
+
];
|
|
19
|
+
function getGoogleOAuthClient(redirectUri, { clientId, clientSecret }) {
|
|
20
|
+
if (!clientId || !clientSecret) throw new Error("Google OAuth credentials not configured");
|
|
21
|
+
return new OAuth2Client(clientId, clientSecret, redirectUri);
|
|
22
|
+
}
|
|
23
|
+
function loginErrorRedirect(siteUrl, error) {
|
|
24
|
+
return NextResponse.redirect(`${siteUrl}${GOOGLE_OAUTH_LOGIN_PATH}?error=${encodeURIComponent(error)}`);
|
|
25
|
+
}
|
|
26
|
+
/** GET `/api/auth/google` — start the Google OAuth redirect. */
|
|
27
|
+
async function handleGoogleOAuthStart(clientConfig) {
|
|
28
|
+
try {
|
|
29
|
+
const authorizeUrl = getGoogleOAuthClient(`${getSiteUrl()}${GOOGLE_OAUTH_CALLBACK_PATH}`, clientConfig).generateAuthUrl({
|
|
30
|
+
access_type: "offline",
|
|
31
|
+
scope: [...GOOGLE_OAUTH_SCOPES],
|
|
32
|
+
prompt: "select_account"
|
|
33
|
+
});
|
|
34
|
+
return NextResponse.redirect(authorizeUrl);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
log.error(error instanceof Error ? error.message : "OAuth configuration error");
|
|
37
|
+
return NextResponse.json({ error: "OAuth configuration error" }, { status: 500 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** GET `/api/auth/google/callback` — exchange the code and set the Payload session cookie. */
|
|
41
|
+
async function handleGoogleOAuthCallback(request, { config, ...clientConfig }) {
|
|
42
|
+
try {
|
|
43
|
+
const { searchParams } = new URL(request.url);
|
|
44
|
+
const siteUrl = getSiteUrl();
|
|
45
|
+
const code = searchParams.get("code");
|
|
46
|
+
const error = searchParams.get("error");
|
|
47
|
+
if (error) return loginErrorRedirect(siteUrl, error);
|
|
48
|
+
if (!code) return loginErrorRedirect(siteUrl, "no_code");
|
|
49
|
+
const client = getGoogleOAuthClient(`${siteUrl}${GOOGLE_OAUTH_CALLBACK_PATH}`, clientConfig);
|
|
50
|
+
const { tokens } = await client.getToken(code);
|
|
51
|
+
client.setCredentials(tokens);
|
|
52
|
+
if (!tokens.id_token) return loginErrorRedirect(siteUrl, "no_token");
|
|
53
|
+
const googlePayload = (await client.verifyIdToken({
|
|
54
|
+
idToken: tokens.id_token,
|
|
55
|
+
audience: clientConfig.clientId
|
|
56
|
+
})).getPayload();
|
|
57
|
+
if (!googlePayload?.email) return loginErrorRedirect(siteUrl, "no_email");
|
|
58
|
+
const { email, sub: googleId, name, picture } = googlePayload;
|
|
59
|
+
const payload = await getPayload({ config: await config });
|
|
60
|
+
const users = await payload.find({
|
|
61
|
+
collection: "users",
|
|
62
|
+
where: { or: [{ email: { equals: email } }, { googleId: { equals: googleId } }] },
|
|
63
|
+
limit: 1
|
|
64
|
+
});
|
|
65
|
+
let user = users.docs[0];
|
|
66
|
+
if (users.docs.length > 0) {
|
|
67
|
+
user = users.docs[0];
|
|
68
|
+
const googleUser = user;
|
|
69
|
+
const updateData = {};
|
|
70
|
+
if (!googleUser.googleId) {
|
|
71
|
+
updateData.googleId = googleId;
|
|
72
|
+
updateData.name = name || googleUser.name;
|
|
73
|
+
updateData.picture = picture || googleUser.picture;
|
|
74
|
+
}
|
|
75
|
+
if (Object.keys(updateData).length > 0) user = await payload.update({
|
|
76
|
+
collection: "users",
|
|
77
|
+
id: user.id,
|
|
78
|
+
data: updateData
|
|
79
|
+
});
|
|
80
|
+
} else return loginErrorRedirect(siteUrl, "user_not_found");
|
|
81
|
+
const collectionConfig = payload.collections["users"].config;
|
|
82
|
+
const sessionUser = user;
|
|
83
|
+
const { sid } = await addSessionToUser({
|
|
84
|
+
collectionConfig,
|
|
85
|
+
payload,
|
|
86
|
+
req: {
|
|
87
|
+
payload,
|
|
88
|
+
user: sessionUser
|
|
89
|
+
},
|
|
90
|
+
user: sessionUser
|
|
91
|
+
});
|
|
92
|
+
const fieldsToSign = getFieldsToSign({
|
|
93
|
+
collectionConfig,
|
|
94
|
+
email: sessionUser.email,
|
|
95
|
+
sid,
|
|
96
|
+
user: sessionUser
|
|
97
|
+
});
|
|
98
|
+
const tokenExpiration = collectionConfig.auth.tokenExpiration || 7200;
|
|
99
|
+
const secret = payload.secret;
|
|
100
|
+
const { token } = await jwtSign({
|
|
101
|
+
fieldsToSign,
|
|
102
|
+
secret,
|
|
103
|
+
tokenExpiration
|
|
104
|
+
});
|
|
105
|
+
const cookie = generatePayloadCookie({
|
|
106
|
+
collectionAuthConfig: collectionConfig.auth,
|
|
107
|
+
cookiePrefix: payload.config.cookiePrefix || "payload",
|
|
108
|
+
token,
|
|
109
|
+
returnCookieAsObject: true
|
|
110
|
+
});
|
|
111
|
+
const response = NextResponse.redirect(`${siteUrl}${GOOGLE_OAUTH_ADMIN_PATH}`);
|
|
112
|
+
response.cookies.set(cookie.name, cookie.value, {
|
|
113
|
+
httpOnly: cookie.httpOnly,
|
|
114
|
+
sameSite: cookie.sameSite?.toLowerCase(),
|
|
115
|
+
secure: cookie.secure,
|
|
116
|
+
path: cookie.path,
|
|
117
|
+
expires: cookie.expires ? new Date(cookie.expires) : void 0,
|
|
118
|
+
maxAge: cookie.maxAge
|
|
119
|
+
});
|
|
120
|
+
return response;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const errorMessage = error instanceof Error ? error.message : "Authentication failed";
|
|
123
|
+
log.error(errorMessage);
|
|
124
|
+
return loginErrorRedirect(getSiteUrl(), errorMessage);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { GOOGLE_OAUTH_AFTER_LOGIN, handleGoogleOAuthCallback, handleGoogleOAuthStart };
|
|
129
|
+
|
|
130
|
+
//# sourceMappingURL=google-oauth.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google-oauth.mjs","names":[],"sources":["../../src/cms/auth/google-oauth.ts"],"sourcesContent":["import { OAuth2Client } from 'google-auth-library'\nimport { NextRequest, NextResponse } from 'next/server'\nimport { getFieldsToSign, getPayload, jwtSign } from 'payload'\nimport { addSessionToUser, generatePayloadCookie } from 'payload/shared'\n\nimport { getSiteUrl } from '../../seo/site-url.js'\nimport { createLogger } from '../../utils/logger/logger.js'\n\nimport type { Config, SanitizedConfig } from 'payload'\n\nconst log = createLogger('googleOAuth')\n\nconst GOOGLE_OAUTH_CALLBACK_PATH = '/api/auth/google/callback'\nconst GOOGLE_OAUTH_LOGIN_PATH = '/admin/login'\nconst GOOGLE_OAUTH_ADMIN_PATH = '/admin'\n\n/** Payload `admin.components.afterLogin` path for the shared Google button. */\nexport const GOOGLE_OAUTH_AFTER_LOGIN = [\n '@tetherto/dev-websites-core/ui/google-oauth#GoogleOAuthButton',\n] as const\n\nconst GOOGLE_OAUTH_SCOPES = ['openid', 'email', 'profile'] as const\n\nexport type GoogleOAuthClientConfig = {\n clientId: string\n clientSecret: string\n}\n\nexport type HandleGoogleOAuthCallbackOptions = GoogleOAuthClientConfig & {\n /** Site Payload config default export (`import config from '@payload-config'`). */\n config: Config | SanitizedConfig | Promise<Config | SanitizedConfig>\n}\n\nfunction getGoogleOAuthClient(\n redirectUri: string,\n { clientId, clientSecret }: GoogleOAuthClientConfig\n) {\n if (!clientId || !clientSecret) {\n throw new Error('Google OAuth credentials not configured')\n }\n\n return new OAuth2Client(clientId, clientSecret, redirectUri)\n}\n\nfunction loginErrorRedirect(siteUrl: string, error: string) {\n return NextResponse.redirect(\n `${siteUrl}${GOOGLE_OAUTH_LOGIN_PATH}?error=${encodeURIComponent(error)}`\n )\n}\n\n/** GET `/api/auth/google` — start the Google OAuth redirect. */\nexport async function handleGoogleOAuthStart(clientConfig: GoogleOAuthClientConfig) {\n try {\n const siteUrl = getSiteUrl()\n const redirectUri = `${siteUrl}${GOOGLE_OAUTH_CALLBACK_PATH}`\n const client = getGoogleOAuthClient(redirectUri, clientConfig)\n\n const authorizeUrl = client.generateAuthUrl({\n access_type: 'offline',\n scope: [...GOOGLE_OAUTH_SCOPES],\n prompt: 'select_account',\n })\n\n return NextResponse.redirect(authorizeUrl)\n } catch (error) {\n log.error(error instanceof Error ? error.message : 'OAuth configuration error')\n return NextResponse.json({ error: 'OAuth configuration error' }, { status: 500 })\n }\n}\n\n/** GET `/api/auth/google/callback` — exchange the code and set the Payload session cookie. */\nexport async function handleGoogleOAuthCallback(\n request: NextRequest,\n { config, ...clientConfig }: HandleGoogleOAuthCallbackOptions\n) {\n try {\n const { searchParams } = new URL(request.url)\n const siteUrl = getSiteUrl()\n const code = searchParams.get('code')\n const error = searchParams.get('error')\n\n if (error) {\n return loginErrorRedirect(siteUrl, error)\n }\n\n if (!code) {\n return loginErrorRedirect(siteUrl, 'no_code')\n }\n\n const redirectUri = `${siteUrl}${GOOGLE_OAUTH_CALLBACK_PATH}`\n const client = getGoogleOAuthClient(redirectUri, clientConfig)\n\n const { tokens } = await client.getToken(code)\n client.setCredentials(tokens)\n\n if (!tokens.id_token) {\n return loginErrorRedirect(siteUrl, 'no_token')\n }\n\n const ticket = await client.verifyIdToken({\n idToken: tokens.id_token,\n audience: clientConfig.clientId,\n })\n\n const googlePayload = ticket.getPayload()\n if (!googlePayload?.email) {\n return loginErrorRedirect(siteUrl, 'no_email')\n }\n\n const { email, sub: googleId, name, picture } = googlePayload\n const payload = await getPayload({\n config: (await config) as SanitizedConfig,\n })\n\n const users = await payload.find({\n collection: 'users',\n where: {\n or: [{ email: { equals: email } }, { googleId: { equals: googleId } }],\n },\n limit: 1,\n })\n\n let user = users.docs[0]\n if (users.docs.length > 0) {\n user = users.docs[0]\n\n const googleUser = user as typeof user & {\n googleId?: string | null\n name?: string | null\n picture?: string | null\n }\n const updateData: Record<string, unknown> = {}\n\n if (!googleUser.googleId) {\n updateData.googleId = googleId\n updateData.name = name || googleUser.name\n updateData.picture = picture || googleUser.picture\n }\n\n if (Object.keys(updateData).length > 0) {\n user = await payload.update({\n collection: 'users',\n id: user.id,\n data: updateData,\n })\n }\n } else {\n return loginErrorRedirect(siteUrl, 'user_not_found')\n }\n\n const collectionConfig = payload.collections['users'].config\n\n const sessionUser = user as Parameters<typeof addSessionToUser>[0]['user']\n\n const { sid } = await addSessionToUser({\n collectionConfig,\n payload,\n req: {\n payload,\n user: sessionUser,\n } as unknown as Parameters<typeof addSessionToUser>[0]['req'],\n user: sessionUser,\n })\n\n const fieldsToSign = getFieldsToSign({\n collectionConfig,\n email: sessionUser.email as string,\n sid,\n user: sessionUser,\n })\n\n const tokenExpiration = collectionConfig.auth.tokenExpiration || 7200\n const secret = payload.secret\n\n const { token } = await jwtSign({\n fieldsToSign,\n secret,\n tokenExpiration,\n })\n\n const cookie = generatePayloadCookie({\n collectionAuthConfig: collectionConfig.auth,\n cookiePrefix: payload.config.cookiePrefix || 'payload',\n token,\n returnCookieAsObject: true,\n })\n\n const response = NextResponse.redirect(`${siteUrl}${GOOGLE_OAUTH_ADMIN_PATH}`)\n\n response.cookies.set(cookie.name, cookie.value!, {\n httpOnly: cookie.httpOnly,\n sameSite: cookie.sameSite?.toLowerCase() as 'strict' | 'lax' | 'none' | undefined,\n secure: cookie.secure,\n path: cookie.path,\n expires: cookie.expires ? new Date(cookie.expires) : undefined,\n maxAge: cookie.maxAge,\n })\n\n return response\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Authentication failed'\n log.error(errorMessage)\n return loginErrorRedirect(getSiteUrl(), errorMessage)\n }\n}\n"],"mappings":"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"}
|
package/dist/cms/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { $ as createAccessHelpers, A as downloadCtaBlock, B as seoFields, C as codeBlockLanguages, D as sectionHeadingBlock, E as leafBlocks, F as richTextBlock, G as STOCK_COLLECTION_SLUGS, H as BuildCmsConfigOptions, I as coerceBlogPostCategoriesOnRead, J as GlobalOverride, K as StockCollectionSlug, L as coerceBlogPostCategoriesOnWrite, M as imageBlock, N as codeTabsBlock, O as accordionBlock, P as codeBlock, Q as AccessOptions, R as setPublishedAt, S as showcaseContentBlocks, T as languageOptions, U as buildCmsConfig, V as draftsWithAutosave, W as Overrides, X as applyGlobalOverride, Y as applyCollectionOverride, Z as AccessHelpers, _ as requireEnvAtRuntime, a as createChangelog, b as blogContentBlocks, c as createPages, d as createAuthors, f as createMedia, g as createS3MediaPlugin, h as CreateS3MediaPluginOptions, i as createEventTags, j as videoBlock, k as stepsBlock, l as createBlogPosts, m as createUsers, n as createSiteSettings, o as createChangelogTags, p as createApiTokens, q as CollectionOverride, r as createEvents, s as createLegalPages, t as createAnnouncementBanner, u as createCategories, v as getRichTextEditorConfig, w as defaultCodeLanguage, x as pageContentBlocks, y as richTextEditor, z as slugify } from "../index.mjs";
|
|
2
2
|
import { n as readPayloadMediaAsDataUrl, r as readPayloadMediaBuffer, t as PayloadMedia } from "../readMediaBuffer.mjs";
|
|
3
|
-
export { type AccessHelpers, type AccessOptions, type BuildCmsConfigOptions, type CollectionOverride, type CreateS3MediaPluginOptions, type GlobalOverride, type Overrides, type PayloadMedia, STOCK_COLLECTION_SLUGS, type StockCollectionSlug, accordionBlock, applyCollectionOverride, applyGlobalOverride, blogContentBlocks, buildCmsConfig, codeBlock, codeBlockLanguages, codeTabsBlock, createAccessHelpers, createApiTokens, createAuthors, createBlogPosts, createCategories, createChangelog, createChangelogTags, createEventTags, createEvents, createLegalPages, createMedia, createPages, createS3MediaPlugin, createSiteSettings, createUsers, defaultCodeLanguage, downloadCtaBlock, draftsWithAutosave, getRichTextEditorConfig, imageBlock, languageOptions, leafBlocks, pageContentBlocks, readPayloadMediaAsDataUrl, readPayloadMediaBuffer, requireEnvAtRuntime, richTextBlock, richTextEditor, sectionHeadingBlock, seoFields, setPublishedAt, showcaseContentBlocks, slugify, stepsBlock, videoBlock };
|
|
3
|
+
export { type AccessHelpers, type AccessOptions, type BuildCmsConfigOptions, type CollectionOverride, type CreateS3MediaPluginOptions, type GlobalOverride, type Overrides, type PayloadMedia, STOCK_COLLECTION_SLUGS, type StockCollectionSlug, accordionBlock, applyCollectionOverride, applyGlobalOverride, blogContentBlocks, buildCmsConfig, codeBlock, codeBlockLanguages, codeTabsBlock, coerceBlogPostCategoriesOnRead, coerceBlogPostCategoriesOnWrite, createAccessHelpers, createAnnouncementBanner, createApiTokens, createAuthors, createBlogPosts, createCategories, createChangelog, createChangelogTags, createEventTags, createEvents, createLegalPages, createMedia, createPages, createS3MediaPlugin, createSiteSettings, createUsers, defaultCodeLanguage, downloadCtaBlock, draftsWithAutosave, getRichTextEditorConfig, imageBlock, languageOptions, leafBlocks, pageContentBlocks, readPayloadMediaAsDataUrl, readPayloadMediaBuffer, requireEnvAtRuntime, richTextBlock, richTextEditor, sectionHeadingBlock, seoFields, setPublishedAt, showcaseContentBlocks, slugify, stepsBlock, videoBlock };
|
package/dist/cms/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as richTextBlock, B as languageOptions, C as blogContentBlocks, D as accordionBlock, E as sectionHeadingBlock, F as downloadCtaBlock, G as createAccessHelpers, H as seoFields, I as codeTabsBlock, L as codeBlock, M as richTextEditor, N as videoBlock, O as leafBlocks, P as imageBlock, R as codeBlockLanguages, S as coerceBlogPostCategoriesOnWrite, T as showcaseContentBlocks, U as slugify, V as createAuthors, W as createApiTokens, _ as createCategories, a as STOCK_COLLECTION_SLUGS, b as setPublishedAt, c as createSiteSettings, d as createPages, f as createMedia, g as createChangelog, h as createChangelogTags, i as buildCmsConfig, j as getRichTextEditorConfig, k as stepsBlock, l as createUsers, m as createEventTags, n as createS3MediaPlugin, o as applyCollectionOverride, p as createEvents, r as requireEnvAtRuntime, s as applyGlobalOverride, t as createAnnouncementBanner, u as createLegalPages, v as createBlogPosts, w as pageContentBlocks, x as coerceBlogPostCategoriesOnRead, y as draftsWithAutosave, z as defaultCodeLanguage } from "../cms.mjs";
|
|
2
2
|
import { n as readPayloadMediaBuffer, t as readPayloadMediaAsDataUrl } from "../readMediaBuffer.mjs";
|
|
3
|
-
export { STOCK_COLLECTION_SLUGS, accordionBlock, applyCollectionOverride, applyGlobalOverride, blogContentBlocks, buildCmsConfig, codeBlock, codeBlockLanguages, codeTabsBlock, createAccessHelpers, createApiTokens, createAuthors, createBlogPosts, createCategories, createChangelog, createChangelogTags, createEventTags, createEvents, createLegalPages, createMedia, createPages, createS3MediaPlugin, createSiteSettings, createUsers, defaultCodeLanguage, downloadCtaBlock, draftsWithAutosave, getRichTextEditorConfig, imageBlock, languageOptions, leafBlocks, pageContentBlocks, readPayloadMediaAsDataUrl, readPayloadMediaBuffer, requireEnvAtRuntime, richTextBlock, richTextEditor, sectionHeadingBlock, seoFields, setPublishedAt, showcaseContentBlocks, slugify, stepsBlock, videoBlock };
|
|
3
|
+
export { STOCK_COLLECTION_SLUGS, accordionBlock, applyCollectionOverride, applyGlobalOverride, blogContentBlocks, buildCmsConfig, codeBlock, codeBlockLanguages, codeTabsBlock, coerceBlogPostCategoriesOnRead, coerceBlogPostCategoriesOnWrite, createAccessHelpers, createAnnouncementBanner, createApiTokens, createAuthors, createBlogPosts, createCategories, createChangelog, createChangelogTags, createEventTags, createEvents, createLegalPages, createMedia, createPages, createS3MediaPlugin, createSiteSettings, createUsers, defaultCodeLanguage, downloadCtaBlock, draftsWithAutosave, getRichTextEditorConfig, imageBlock, languageOptions, leafBlocks, pageContentBlocks, readPayloadMediaAsDataUrl, readPayloadMediaBuffer, requireEnvAtRuntime, richTextBlock, richTextEditor, sectionHeadingBlock, seoFields, setPublishedAt, showcaseContentBlocks, slugify, stepsBlock, videoBlock };
|
package/dist/cms.mjs
CHANGED
|
@@ -708,6 +708,31 @@ const showcaseContentBlocks = [
|
|
|
708
708
|
imageBlock
|
|
709
709
|
];
|
|
710
710
|
//#endregion
|
|
711
|
+
//#region src/cms/hooks/coerce-blog-post-categories.ts
|
|
712
|
+
function toCategoryIdList(value) {
|
|
713
|
+
if (value == null || value === "") return [];
|
|
714
|
+
return (Array.isArray(value) ? value : [value]).map((item) => {
|
|
715
|
+
if (typeof item === "string" || typeof item === "number") return String(item);
|
|
716
|
+
if (item && typeof item === "object" && "id" in item && item.id != null) return String(item.id);
|
|
717
|
+
return null;
|
|
718
|
+
}).filter((id) => Boolean(id));
|
|
719
|
+
}
|
|
720
|
+
/** Posts saved before `category` became `hasMany` still store a single value. */
|
|
721
|
+
const coerceBlogPostCategoriesOnWrite = ({ data }) => {
|
|
722
|
+
if (!data || !("category" in data)) return data;
|
|
723
|
+
data.category = toCategoryIdList(data.category);
|
|
724
|
+
return data;
|
|
725
|
+
};
|
|
726
|
+
const coerceBlogPostCategoriesOnRead = ({ doc }) => {
|
|
727
|
+
if (!doc) return doc;
|
|
728
|
+
if (doc.category == null || doc.category === "") {
|
|
729
|
+
doc.category = [];
|
|
730
|
+
return doc;
|
|
731
|
+
}
|
|
732
|
+
if (!Array.isArray(doc.category)) doc.category = [doc.category];
|
|
733
|
+
return doc;
|
|
734
|
+
};
|
|
735
|
+
//#endregion
|
|
711
736
|
//#region src/cms/hooks/set-published-at.ts
|
|
712
737
|
const setPublishedAt = ({ data }) => {
|
|
713
738
|
if (data && !data.publishedAt && data._status === "published") data.publishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -746,7 +771,10 @@ function createBlogPosts(access) {
|
|
|
746
771
|
delete: access.adminOnly
|
|
747
772
|
},
|
|
748
773
|
versions: draftsWithAutosave,
|
|
749
|
-
hooks: {
|
|
774
|
+
hooks: {
|
|
775
|
+
beforeChange: [setPublishedAt, coerceBlogPostCategoriesOnWrite],
|
|
776
|
+
afterRead: [coerceBlogPostCategoriesOnRead]
|
|
777
|
+
},
|
|
750
778
|
fields: [
|
|
751
779
|
{
|
|
752
780
|
name: "title",
|
|
@@ -798,6 +826,8 @@ function createBlogPosts(access) {
|
|
|
798
826
|
name: "category",
|
|
799
827
|
type: "relationship",
|
|
800
828
|
relationTo: "categories",
|
|
829
|
+
hasMany: true,
|
|
830
|
+
label: "Categories",
|
|
801
831
|
admin: { position: "sidebar" }
|
|
802
832
|
},
|
|
803
833
|
{
|
|
@@ -1468,7 +1498,7 @@ function createUsers(access) {
|
|
|
1468
1498
|
return {
|
|
1469
1499
|
slug: "users",
|
|
1470
1500
|
admin: { useAsTitle: "email" },
|
|
1471
|
-
auth:
|
|
1501
|
+
auth: { verify: false },
|
|
1472
1502
|
access: {
|
|
1473
1503
|
read: access.adminOnly,
|
|
1474
1504
|
create: access.adminOnly,
|
|
@@ -1476,7 +1506,29 @@ function createUsers(access) {
|
|
|
1476
1506
|
delete: access.adminOnly,
|
|
1477
1507
|
admin: access.adminOnly
|
|
1478
1508
|
},
|
|
1479
|
-
fields: [
|
|
1509
|
+
fields: [
|
|
1510
|
+
{
|
|
1511
|
+
name: "googleId",
|
|
1512
|
+
type: "text",
|
|
1513
|
+
unique: true,
|
|
1514
|
+
index: true,
|
|
1515
|
+
admin: {
|
|
1516
|
+
position: "sidebar",
|
|
1517
|
+
readOnly: true,
|
|
1518
|
+
description: "Google OAuth user ID"
|
|
1519
|
+
}
|
|
1520
|
+
},
|
|
1521
|
+
{
|
|
1522
|
+
name: "name",
|
|
1523
|
+
type: "text",
|
|
1524
|
+
admin: { description: "Display name from Google" }
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
name: "picture",
|
|
1528
|
+
type: "text",
|
|
1529
|
+
admin: { description: "Profile picture URL from Google" }
|
|
1530
|
+
}
|
|
1531
|
+
]
|
|
1480
1532
|
};
|
|
1481
1533
|
}
|
|
1482
1534
|
//#endregion
|
|
@@ -1644,8 +1696,13 @@ function buildCmsConfig(options) {
|
|
|
1644
1696
|
const collections = [...STOCK_COLLECTION_SLUGS.map((slug) => applyCollectionOverride(stock[slug], overrides[slug])).filter((c) => c !== null), ...options.extraCollections ?? []];
|
|
1645
1697
|
const siteSettings = applyGlobalOverride(createSiteSettings(access), overrides["site-settings"]);
|
|
1646
1698
|
const globals = [...siteSettings ? [siteSettings] : [], ...options.extraGlobals ?? []];
|
|
1699
|
+
const { jobs: jobsOverride, ...payloadRest } = options.payload ?? {};
|
|
1647
1700
|
return buildConfig({
|
|
1648
|
-
...
|
|
1701
|
+
...payloadRest,
|
|
1702
|
+
jobs: {
|
|
1703
|
+
autoRun: [{ cron: "* * * * *" }],
|
|
1704
|
+
...jobsOverride
|
|
1705
|
+
},
|
|
1649
1706
|
...defined({
|
|
1650
1707
|
sharp: options.sharp,
|
|
1651
1708
|
cors: options.cors,
|
|
@@ -1712,6 +1769,80 @@ function createS3MediaPlugin(options = {}) {
|
|
|
1712
1769
|
});
|
|
1713
1770
|
}
|
|
1714
1771
|
//#endregion
|
|
1715
|
-
|
|
1772
|
+
//#region src/cms/globals/createAnnouncementBanner.ts
|
|
1773
|
+
const validateBannerId = (value, { siblingData }) => {
|
|
1774
|
+
if (siblingData?.enabled && !String(value ?? "").trim()) return "Banner ID is required when the banner is enabled.";
|
|
1775
|
+
return true;
|
|
1776
|
+
};
|
|
1777
|
+
/** Announcement banner global (no site revalidation — add via applyGlobalOverride hooks). */
|
|
1778
|
+
function createAnnouncementBanner(access) {
|
|
1779
|
+
return {
|
|
1780
|
+
slug: "announcement-banner",
|
|
1781
|
+
label: "Announcement Banner",
|
|
1782
|
+
admin: { description: "Sticky site-wide banner shown above the main navigation. Bump Banner ID when replacing content so users who dismissed an older message in the current session see the new one." },
|
|
1783
|
+
access: {
|
|
1784
|
+
read: access.publicRead,
|
|
1785
|
+
update: access.adminOnly
|
|
1786
|
+
},
|
|
1787
|
+
fields: [
|
|
1788
|
+
{
|
|
1789
|
+
name: "enabled",
|
|
1790
|
+
type: "checkbox",
|
|
1791
|
+
defaultValue: false,
|
|
1792
|
+
admin: { description: "When off, the banner is hidden on the public site." }
|
|
1793
|
+
},
|
|
1794
|
+
{
|
|
1795
|
+
name: "bannerId",
|
|
1796
|
+
type: "text",
|
|
1797
|
+
admin: { description: "Stable identifier for this banner version. Change it when publishing new copy so users who dismissed an older banner in this session see the update." },
|
|
1798
|
+
validate: validateBannerId
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
type: "row",
|
|
1802
|
+
fields: [{
|
|
1803
|
+
name: "prefix",
|
|
1804
|
+
type: "text",
|
|
1805
|
+
localized: true,
|
|
1806
|
+
maxLength: 8,
|
|
1807
|
+
admin: {
|
|
1808
|
+
width: "25%",
|
|
1809
|
+
description: "Accent label (max 8 characters), e.g. \"New:\""
|
|
1810
|
+
}
|
|
1811
|
+
}, {
|
|
1812
|
+
name: "description",
|
|
1813
|
+
type: "text",
|
|
1814
|
+
localized: true,
|
|
1815
|
+
maxLength: 1e3,
|
|
1816
|
+
admin: {
|
|
1817
|
+
width: "75%",
|
|
1818
|
+
description: "Banner message (max 1000 characters)."
|
|
1819
|
+
}
|
|
1820
|
+
}]
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
type: "row",
|
|
1824
|
+
fields: [{
|
|
1825
|
+
name: "ctaLabel",
|
|
1826
|
+
type: "text",
|
|
1827
|
+
localized: true,
|
|
1828
|
+
maxLength: 18,
|
|
1829
|
+
admin: {
|
|
1830
|
+
width: "50%",
|
|
1831
|
+
description: "Optional button label (max 18 characters)."
|
|
1832
|
+
}
|
|
1833
|
+
}, {
|
|
1834
|
+
name: "ctaHref",
|
|
1835
|
+
type: "text",
|
|
1836
|
+
admin: {
|
|
1837
|
+
width: "50%",
|
|
1838
|
+
description: "Optional button URL. Relative paths stay in-site."
|
|
1839
|
+
}
|
|
1840
|
+
}]
|
|
1841
|
+
}
|
|
1842
|
+
]
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
//#endregion
|
|
1846
|
+
export { richTextBlock as A, languageOptions as B, blogContentBlocks as C, accordionBlock as D, sectionHeadingBlock as E, downloadCtaBlock as F, createAccessHelpers as G, seoFields as H, codeTabsBlock as I, codeBlock as L, richTextEditor as M, videoBlock as N, leafBlocks as O, imageBlock as P, codeBlockLanguages as R, coerceBlogPostCategoriesOnWrite as S, showcaseContentBlocks as T, slugify as U, createAuthors as V, createApiTokens as W, createCategories as _, STOCK_COLLECTION_SLUGS as a, setPublishedAt as b, createSiteSettings as c, createPages as d, createMedia as f, createChangelog as g, createChangelogTags as h, buildCmsConfig as i, getRichTextEditorConfig as j, stepsBlock as k, createUsers as l, createEventTags as m, createS3MediaPlugin as n, applyCollectionOverride as o, createEvents as p, requireEnvAtRuntime as r, applyGlobalOverride as s, createAnnouncementBanner as t, createLegalPages as u, createBlogPosts as v, pageContentBlocks as w, coerceBlogPostCategoriesOnRead as x, draftsWithAutosave as y, defaultCodeLanguage as z };
|
|
1716
1847
|
|
|
1717
1848
|
//# sourceMappingURL=cms.mjs.map
|