@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.
Files changed (35) hide show
  1. package/README.md +72 -0
  2. package/dist/cms/google-oauth.d.mts +20 -0
  3. package/dist/cms/google-oauth.d.mts.map +1 -0
  4. package/dist/cms/google-oauth.mjs +130 -0
  5. package/dist/cms/google-oauth.mjs.map +1 -0
  6. package/dist/cms/index.d.mts +2 -2
  7. package/dist/cms/index.mjs +2 -2
  8. package/dist/cms.mjs +136 -5
  9. package/dist/cms.mjs.map +1 -1
  10. package/dist/index.d.mts +11 -2
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +4 -3
  13. package/dist/index5.d.mts +2 -2
  14. package/dist/seo/index.mjs +2 -1
  15. package/dist/seo.mjs +2 -13
  16. package/dist/seo.mjs.map +1 -1
  17. package/dist/site-url.mjs +16 -0
  18. package/dist/site-url.mjs.map +1 -0
  19. package/dist/ui/components/Button/Button.d.mts +1 -1
  20. package/dist/ui/components/GoogleOAuthButton/GoogleOAuthButton.d.mts +5 -0
  21. package/dist/ui/components/GoogleOAuthButton/GoogleOAuthButton.d.mts.map +1 -0
  22. package/dist/ui/components/GoogleOAuthButton/GoogleOAuthButton.mjs +72 -0
  23. package/dist/ui/components/GoogleOAuthButton/GoogleOAuthButton.mjs.map +1 -0
  24. package/dist/ui/components/GoogleOAuthButton/googleOAuthStartPath.d.mts +6 -0
  25. package/dist/ui/components/GoogleOAuthButton/googleOAuthStartPath.d.mts.map +1 -0
  26. package/dist/ui/components/GoogleOAuthButton/googleOAuthStartPath.mjs +7 -0
  27. package/dist/ui/components/GoogleOAuthButton/googleOAuthStartPath.mjs.map +1 -0
  28. package/dist/ui/components/GoogleOAuthButton/index.d.mts +2 -0
  29. package/dist/ui/components/GoogleOAuthButton/index.mjs +2 -0
  30. package/dist/ui/index.d.mts +3 -1
  31. package/dist/ui/index.mjs +1 -0
  32. package/package.json +18 -2
  33. package/src/ui/components/GoogleOAuthButton/GoogleOAuthButton.tsx +69 -0
  34. package/src/ui/components/GoogleOAuthButton/googleOAuthStartPath.ts +2 -0
  35. 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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"}
@@ -1,3 +1,3 @@
1
- import { A as videoBlock, B as buildCmsConfig, C as defaultCodeLanguage, D as accordionBlock, E as sectionHeadingBlock, F as setPublishedAt, G as GlobalOverride, H as STOCK_COLLECTION_SLUGS, I as slugify, J as AccessHelpers, K as applyCollectionOverride, L as seoFields, M as codeTabsBlock, N as codeBlock, O as stepsBlock, P as richTextBlock, R as draftsWithAutosave, S as codeBlockLanguages, T as leafBlocks, U as StockCollectionSlug, V as Overrides, W as CollectionOverride, X as createAccessHelpers, Y as AccessOptions, _ as getRichTextEditorConfig, a as createChangelogTags, b as pageContentBlocks, c as createBlogPosts, d as createMedia, f as createApiTokens, g as requireEnvAtRuntime, h as createS3MediaPlugin, i as createChangelog, j as imageBlock, k as downloadCtaBlock, l as createCategories, m as CreateS3MediaPluginOptions, n as createEvents, o as createLegalPages, p as createUsers, q as applyGlobalOverride, r as createEventTags, s as createPages, t as createSiteSettings, u as createAuthors, v as richTextEditor, w as languageOptions, x as showcaseContentBlocks, y as blogContentBlocks, z as BuildCmsConfigOptions } from "../index.mjs";
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 };
@@ -1,3 +1,3 @@
1
- import { A as videoBlock, B as slugify, C as sectionHeadingBlock, D as richTextBlock, E as stepsBlock, F as codeBlockLanguages, H as createAccessHelpers, I as defaultCodeLanguage, L as languageOptions, M as downloadCtaBlock, N as codeTabsBlock, O as getRichTextEditorConfig, P as codeBlock, R as createAuthors, S as showcaseContentBlocks, T as leafBlocks, V as createApiTokens, _ as createBlogPosts, a as applyCollectionOverride, b as blogContentBlocks, c as createUsers, d as createMedia, f as createEvents, g as createCategories, h as createChangelog, i as STOCK_COLLECTION_SLUGS, j as imageBlock, k as richTextEditor, l as createLegalPages, m as createChangelogTags, n as requireEnvAtRuntime, o as applyGlobalOverride, p as createEventTags, r as buildCmsConfig, s as createSiteSettings, t as createS3MediaPlugin, u as createPages, v as draftsWithAutosave, w as accordionBlock, x as pageContentBlocks, y as setPublishedAt, z as seoFields } from "../cms.mjs";
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: { beforeChange: [setPublishedAt] },
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: true,
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
- ...options.payload,
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
- export { videoBlock as A, slugify as B, sectionHeadingBlock as C, richTextBlock as D, stepsBlock as E, codeBlockLanguages as F, createAccessHelpers as H, defaultCodeLanguage as I, languageOptions as L, downloadCtaBlock as M, codeTabsBlock as N, getRichTextEditorConfig as O, codeBlock as P, createAuthors as R, showcaseContentBlocks as S, leafBlocks as T, createApiTokens as V, createBlogPosts as _, applyCollectionOverride as a, blogContentBlocks as b, createUsers as c, createMedia as d, createEvents as f, createCategories as g, createChangelog as h, STOCK_COLLECTION_SLUGS as i, imageBlock as j, richTextEditor as k, createLegalPages as l, createChangelogTags as m, requireEnvAtRuntime as n, applyGlobalOverride as o, createEventTags as p, buildCmsConfig as r, createSiteSettings as s, createS3MediaPlugin as t, createPages as u, draftsWithAutosave as v, accordionBlock as w, pageContentBlocks as x, setPublishedAt as y, seoFields as z };
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