Compare commits
2
Commits
7cef95225a
...
471ec4b12a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
471ec4b12a | ||
|
|
060a61fd41 |
Vendored
+103
-36
@@ -1,76 +1,143 @@
|
||||
/*
|
||||
* Starter stylesheet for a sitemap served via buildSitemapXml({ cssUrl }).
|
||||
* Uses type="text/css" on XML (W3C "Associating Style Sheets with XML") — NOT
|
||||
* XSLT, so no browser deprecation warning. Selectors are the XML tag names.
|
||||
* Universal, minimalist & elegant stylesheet for XML Sitemap.
|
||||
* Neutral palette with automatic dark and light mode support.
|
||||
* W3C standard: type="text/css" on XML (zero browser deprecation warnings).
|
||||
*
|
||||
* Copy to your project's /public/sitemap.css and adjust colors/spacing to taste.
|
||||
* Crawlers ignore this; it only affects the human-readable browser view.
|
||||
* Ships with @intecion/ipal-kit. Copy to /public/sitemap.css and adjust freely.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--bg: #fafafa;
|
||||
--card: #ffffff;
|
||||
--border: #e5e7eb;
|
||||
--border-hover: #d1d5db;
|
||||
--text-main: #111827;
|
||||
--text-secondary: #4b5563;
|
||||
--text-muted: #9ca3af;
|
||||
--url-color: #1e293b;
|
||||
--badge-bg: #f3f4f6;
|
||||
--badge-border: #e5e7eb;
|
||||
--badge-text: #4b5563;
|
||||
--accent: #f97316;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #090a0f;
|
||||
--card: #12131a;
|
||||
--border: #1e202e;
|
||||
--border-hover: #2e3247;
|
||||
--text-main: #f9fafb;
|
||||
--text-secondary: #9ca3af;
|
||||
--text-muted: #6b7280;
|
||||
--url-color: #f3f4f6;
|
||||
--badge-bg: #1a1c26;
|
||||
--badge-border: #282b3d;
|
||||
--badge-text: #9ca3af;
|
||||
--accent: #fb923c;
|
||||
}
|
||||
}
|
||||
|
||||
urlset {
|
||||
display: block;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #090d16;
|
||||
color: #f1f5f9;
|
||||
padding: 2rem 1.5rem;
|
||||
max-width: 1200px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, Helvetica, Arial, sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--text-main);
|
||||
padding: 3rem 1.5rem;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Each URL entry as a card. */
|
||||
/* Minimalist header */
|
||||
urlset::before {
|
||||
content: "XML Sitemap";
|
||||
display: block;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--text-main);
|
||||
padding-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
/* Brand note under the header — crafted by Intecion Group */
|
||||
urlset::after {
|
||||
content: "Intecion.com, Technology — engineered for modern digital experiences.";
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
padding-bottom: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* URL card */
|
||||
url {
|
||||
display: block;
|
||||
background: #111827;
|
||||
border: 1px solid #1e293b;
|
||||
background-color: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.65rem;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
/* The URL itself. */
|
||||
url:hover {
|
||||
border-color: var(--border-hover);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
/* URL address */
|
||||
loc {
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #f97316;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--url-color);
|
||||
word-break: break-all;
|
||||
line-height: 1.45;
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
/* Metadata line: lastmod / changefreq / priority, each with a label. */
|
||||
/* Metadata row */
|
||||
lastmod,
|
||||
changefreq,
|
||||
priority {
|
||||
display: inline-block;
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
margin-right: 1.5rem;
|
||||
font-size: 0.775rem;
|
||||
color: var(--text-secondary);
|
||||
margin-right: 1.25rem;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
lastmod::before {
|
||||
content: 'Ostatnia modyfikacja: ';
|
||||
color: #64748b;
|
||||
content: "Zaktualizowano: ";
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
changefreq::before {
|
||||
content: 'Częstotliwość: ';
|
||||
color: #64748b;
|
||||
content: "Częstotliwość: ";
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
priority::before {
|
||||
content: 'Priorytet: ';
|
||||
color: #64748b;
|
||||
content: "Priorytet: ";
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* hreflang alternates as small pills. */
|
||||
/* Alternate language pills */
|
||||
link {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
background: #1e293b;
|
||||
color: #38bdf8;
|
||||
border: 1px solid #334155;
|
||||
padding: 0.15rem 0.45rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
background-color: var(--badge-bg);
|
||||
border: 1px solid var(--badge-border);
|
||||
color: var(--badge-text);
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
margin: 0.4rem 0.35rem 0 0;
|
||||
margin-right: 0.3rem;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
Vendored
+38
-4
@@ -42,6 +42,44 @@ import { buildSeoPlugin, injectAutoFillMeta, injectSeoTabs } from './modules/seo
|
||||
let config = {
|
||||
...incomingConfig
|
||||
};
|
||||
// --- custom admin route (e.g. '/its' instead of '/admin') ---
|
||||
// Sets config.routes.admin; the project must move its app/(payload)/<route>/
|
||||
// folder to match (plugin can't create files in the project's app/).
|
||||
if (options.adminRoute) {
|
||||
config.routes = {
|
||||
...config.routes ?? {},
|
||||
admin: options.adminRoute
|
||||
};
|
||||
}
|
||||
// --- enforced 2FA (TOTP) via @clocklimited/payload-2fa ---
|
||||
// Enforced by default (forceSetup) unless twoFactor is explicitly false. The
|
||||
// plugin is a PEER dependency imported dynamically — ipal-kit doesn't bundle
|
||||
// it, so projects that opt out (twoFactor: false) needn't install it, and the
|
||||
// import never runs under generate:importmap when 2FA is off. Wrapping access
|
||||
// control (not just admin UI) means TOTP gates data access — no API bypass.
|
||||
if (options.twoFactor !== false) {
|
||||
const tf = options.twoFactor;
|
||||
if (!tf?.issuer) {
|
||||
throw new Error('[ipal] twoFactor.issuer is required (name shown in the authenticator ' + 'app). Pass twoFactor: { issuer: "Your Site" }, or twoFactor: false to ' + 'opt out (discouraged).');
|
||||
}
|
||||
try {
|
||||
// Dynamic specifier via a variable so TS doesn't try to resolve this
|
||||
// optional peer dependency at build time (it isn't in the plugin's own
|
||||
// node_modules). Avoids TS2307 without @ts-expect-error; the module
|
||||
// exists at runtime in projects that installed it.
|
||||
const pkg = '@clocklimited/payload-2fa';
|
||||
const { totpPlugin } = await import(pkg);
|
||||
config = await totpPlugin({
|
||||
collection: tf.collectionSlug ?? 'users',
|
||||
forceSetup: true,
|
||||
totp: {
|
||||
issuer: tf.issuer
|
||||
}
|
||||
})(config);
|
||||
} catch (err) {
|
||||
throw new Error('[ipal] 2FA is enforced but @clocklimited/payload-2fa is not installed. ' + 'Run: pnpm add @clocklimited/payload-2fa — or set twoFactor: false to ' + `opt out (discouraged). Original error: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
// --- i18n ---
|
||||
config.localization = buildLocalizationConfig(options.i18n);
|
||||
// --- access: inject roles into the client's auth collection ---
|
||||
@@ -88,10 +126,6 @@ import { buildSeoPlugin, injectAutoFillMeta, injectSeoTabs } from './modules/seo
|
||||
buildCookieSettings(),
|
||||
buildNotifications()
|
||||
];
|
||||
// --- endpoints ---
|
||||
// Test-email endpoint (admin-only): POST /api/ipal/test-email sends a probe
|
||||
// message through the currently selected transport, so the panel's "send
|
||||
// test" button can confirm delivery without leaving the admin UI.
|
||||
config.endpoints = [
|
||||
...config.endpoints ?? [],
|
||||
testEmailEndpoint
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+27
@@ -15,6 +15,17 @@ export type IpalOptions = {
|
||||
* (admin > editor > user) into the client's auth collection.
|
||||
*/
|
||||
access?: AccessOption;
|
||||
/**
|
||||
* Custom admin panel route, e.g. '/its' instead of the default '/admin'.
|
||||
* The plugin sets config.routes.admin. IMPORTANT: this alone isn't enough —
|
||||
* the project must ALSO move its panel folder to match:
|
||||
* app/(payload)/its/[[...segments]]/page.tsx (and the not-found). The plugin
|
||||
* can't create files in the project's app/. See docs/security.md.
|
||||
*
|
||||
* This is obscurity, not security: it hides the panel from dumb bots scanning
|
||||
* /admin, but real protection is strong auth + 2FA + rate limiting.
|
||||
*/
|
||||
adminRoute?: string;
|
||||
/**
|
||||
* Collections whose entries live under an archive page — blog posts, case
|
||||
* studies, anything with a listing. Adds an "archive page" assignment per
|
||||
@@ -45,4 +56,20 @@ export type IpalOptions = {
|
||||
seo?: SeoOption;
|
||||
/** Additional fields injected into SiteSettings global */
|
||||
siteSettingsFields?: Field[];
|
||||
/**
|
||||
* Two-factor authentication (TOTP), ENFORCED for every user. Wires
|
||||
* @clocklimited/payload-2fa with forceSetup — no per-project opt-out. Every
|
||||
* user must configure an authenticator app after login; TOTP is checked before
|
||||
* data access (not just the admin UI). Requires the peer dep installed and an
|
||||
* issuer name (shown in the authenticator app).
|
||||
*
|
||||
* Pass `false` ONLY for a project that genuinely can't use 2FA (rare, discouraged)
|
||||
* — default is enforced. See docs/security.md.
|
||||
*/
|
||||
twoFactor?: {
|
||||
/** Auth collection slug. Defaults to 'users'. */
|
||||
collectionSlug?: string;
|
||||
/** Name shown in the authenticator app (e.g. company/site name). */
|
||||
issuer: string;
|
||||
} | false;
|
||||
};
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nimport type { AccessOption } from './modules/access/types.js'\nimport type { ContentOption } from './modules/content/types.js'\nimport type { FormsOption } from './modules/forms/types.js'\nimport type { I18nConfig } from './modules/i18n/types.js'\nimport type { PagesOption } from './modules/pages/types.js'\nimport type { SeoOption } from './modules/seo/types.js'\n\n/**\n * Configuration options for the IPAL plugin.\n * Passed by the client project in payload.config.ts.\n */\nexport type IpalOptions = {\n /**\n * Role-based access control. Injects a fixed `roles` field\n * (admin > editor > user) into the client's auth collection.\n */\n access?: AccessOption\n\n /**\n * Collections whose entries live under an archive page — blog posts, case\n * studies, anything with a listing. Adds an \"archive page\" assignment per\n * collection in SiteSettings; the assigned page's localized slug becomes the\n * URL segment (/pl/artykuly/moj-post, /en/articles/my-post). Requires `pages`.\n */\n content?: ContentOption\n\n /** Disable the plugin without uninstalling (keeps DB schema intact) */\n enabled?: boolean\n\n /**\n * Forms — form-builder collections (forms, form-submissions) plus the\n * callable submitForm (Turnstile + persistence + SMTP-from-panel email).\n */\n forms?: FormsOption\n\n /** Internationalization — locales, default locale, fallback behavior */\n i18n: I18nConfig\n\n /** Additional fields injected into SiteIntegrations global */\n integrationsFields?: Field[]\n\n /**\n * System-page assignments (homepage, privacy, cookies) in SiteSettings.\n * Provide the slug of the client's Pages collection to enable.\n */\n pages?: PagesOption\n\n /**\n * SEO — adds meta fields to chosen collections (via @payloadcms/plugin-seo)\n * and enables locale-aware metadata helpers.\n */\n seo?: SeoOption\n\n /** Additional fields injected into SiteSettings global */\n siteSettingsFields?: Field[]\n}\n"],"names":[],"mappings":"AASA;;;CAGC,GACD,WA4CC"}
|
||||
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nimport type { AccessOption } from './modules/access/types.js'\nimport type { ContentOption } from './modules/content/types.js'\nimport type { FormsOption } from './modules/forms/types.js'\nimport type { I18nConfig } from './modules/i18n/types.js'\nimport type { PagesOption } from './modules/pages/types.js'\nimport type { SeoOption } from './modules/seo/types.js'\n\n/**\n * Configuration options for the IPAL plugin.\n * Passed by the client project in payload.config.ts.\n */\nexport type IpalOptions = {\n /**\n * Role-based access control. Injects a fixed `roles` field\n * (admin > editor > user) into the client's auth collection.\n */\n access?: AccessOption\n\n /**\n * Custom admin panel route, e.g. '/its' instead of the default '/admin'.\n * The plugin sets config.routes.admin. IMPORTANT: this alone isn't enough —\n * the project must ALSO move its panel folder to match:\n * app/(payload)/its/[[...segments]]/page.tsx (and the not-found). The plugin\n * can't create files in the project's app/. See docs/security.md.\n *\n * This is obscurity, not security: it hides the panel from dumb bots scanning\n * /admin, but real protection is strong auth + 2FA + rate limiting.\n */\n adminRoute?: string\n\n /**\n * Collections whose entries live under an archive page — blog posts, case\n * studies, anything with a listing. Adds an \"archive page\" assignment per\n * collection in SiteSettings; the assigned page's localized slug becomes the\n * URL segment (/pl/artykuly/moj-post, /en/articles/my-post). Requires `pages`.\n */\n content?: ContentOption\n\n /** Disable the plugin without uninstalling (keeps DB schema intact) */\n enabled?: boolean\n\n /**\n * Forms — form-builder collections (forms, form-submissions) plus the\n * callable submitForm (Turnstile + persistence + SMTP-from-panel email).\n */\n forms?: FormsOption\n\n /** Internationalization — locales, default locale, fallback behavior */\n i18n: I18nConfig\n\n /** Additional fields injected into SiteIntegrations global */\n integrationsFields?: Field[]\n\n /**\n * System-page assignments (homepage, privacy, cookies) in SiteSettings.\n * Provide the slug of the client's Pages collection to enable.\n */\n pages?: PagesOption\n\n /**\n * SEO — adds meta fields to chosen collections (via @payloadcms/plugin-seo)\n * and enables locale-aware metadata helpers.\n */\n seo?: SeoOption\n\n /** Additional fields injected into SiteSettings global */\n siteSettingsFields?: Field[]\n\n /**\n * Two-factor authentication (TOTP), ENFORCED for every user. Wires\n * @clocklimited/payload-2fa with forceSetup — no per-project opt-out. Every\n * user must configure an authenticator app after login; TOTP is checked before\n * data access (not just the admin UI). Requires the peer dep installed and an\n * issuer name (shown in the authenticator app).\n *\n * Pass `false` ONLY for a project that genuinely can't use 2FA (rare, discouraged)\n * — default is enforced. See docs/security.md.\n */\n twoFactor?:\n | {\n /** Auth collection slug. Defaults to 'users'. */\n collectionSlug?: string\n /** Name shown in the authenticator app (e.g. company/site name). */\n issuer: string\n }\n | false\n}\n"],"names":[],"mappings":"AASA;;;CAGC,GACD,WA2EC"}
|
||||
@@ -198,3 +198,104 @@ NIE wymuszaj `require-trusted-types-for 'script'`. Powód:
|
||||
- Zysk bezpieczeństwa nie równoważy ryzyka zepsucia strony
|
||||
|
||||
Zostaw Trusted Types poza CSP, dopóki Next/React nie da natywnego wsparcia.
|
||||
|
||||
## Zmiana ścieżki panelu admina (/admin → /its)
|
||||
|
||||
Ukrycie panelu przed botami skanującymi znane ścieżki (`/admin`, `/wp-admin`).
|
||||
Plugin ustawia ścieżkę przez opcję `adminRoute`:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
ipalKit({
|
||||
i18n: i18nConfig,
|
||||
adminRoute: '/its', // panel pod /its zamiast /admin
|
||||
})
|
||||
```
|
||||
|
||||
### WYMAGANE — przenieś folder panelu w projekcie
|
||||
|
||||
Plugin ustawia `config.routes.admin`, ale NIE tworzy plików w `app/` projektu.
|
||||
Musisz przenieść folder panelu, żeby ścieżka zadziałała:
|
||||
|
||||
```
|
||||
# PRZED:
|
||||
app/(payload)/admin/[[...segments]]/page.tsx
|
||||
app/(payload)/admin/[[...segments]]/not-found.tsx
|
||||
|
||||
# PO (nazwa folderu = adminRoute bez ukośnika):
|
||||
app/(payload)/its/[[...segments]]/page.tsx
|
||||
app/(payload)/its/[[...segments]]/not-found.tsx
|
||||
```
|
||||
|
||||
Bez przeniesienia folderu: `config.routes.admin = '/its'`, ale `/its` daje 404
|
||||
(brak pliku), a `/admin` też nie działa (config zmieniony). Oba muszą się zgadzać.
|
||||
|
||||
### To OBSCURITY, nie SECURITY
|
||||
|
||||
Zmiana ścieżki utrudnia automatyczne skany, ale NIE jest zabezpieczeniem.
|
||||
Prawdziwa ochrona panelu:
|
||||
- **2FA** dla każdego użytkownika (planowane — wymuszenie przez plugin)
|
||||
- Silne hasła
|
||||
- Rate limiting na logowaniu
|
||||
- IP allowlist (jeśli panel tylko dla zespołu)
|
||||
- buildSecurityHeaders (nagłówki)
|
||||
|
||||
Zmiana `/admin → /its` to warstwa (odsiewa głupie boty), nie zamek. Traktuj jako
|
||||
dodatek do prawdziwych zabezpieczeń, nie zamiast nich.
|
||||
|
||||
## 2FA (TOTP) — WYMUSZONE dla każdego użytkownika
|
||||
|
||||
Plugin wymusza dwuskładnikowe uwierzytelnianie (TOTP) dla WSZYSTKICH użytkowników
|
||||
panelu — bez możliwości wyłączenia per użytkownik. Każdy projekt ma to z automatu.
|
||||
Używa sprawdzonego `@clocklimited/payload-2fa` (wrapuje access control — TOTP
|
||||
sprawdzane przed dostępem do DANYCH, nie tylko UI panelu).
|
||||
|
||||
### Zależność
|
||||
|
||||
```bash
|
||||
pnpm add @clocklimited/payload-2fa
|
||||
```
|
||||
To PEER dependency — ipal-kit importuje ją dynamicznie tylko gdy 2FA włączone
|
||||
(domyślnie). Bez niej i z włączonym 2FA plugin rzuci jasny błąd.
|
||||
|
||||
### Konfiguracja (payload.config.ts)
|
||||
|
||||
```ts
|
||||
ipalKit({
|
||||
i18n: i18nConfig,
|
||||
twoFactor: {
|
||||
issuer: 'Nazwa Firmy', // pokazywane w aplikacji authenticator (Google Auth itp.)
|
||||
// collectionSlug: 'users', // domyślnie 'users'
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Plugin ustawia `forceSetup: true` — każdy użytkownik MUSI skonfigurować TOTP po
|
||||
zalogowaniu (przekierowanie na setup). Nie ma opcji „włącz/wyłącz" dla użytkownika.
|
||||
|
||||
### Wyłączenie (ODRADZANE)
|
||||
|
||||
```ts
|
||||
twoFactor: false // TYLKO gdy projekt naprawdę nie może użyć 2FA (rzadkie)
|
||||
```
|
||||
Domyślnie 2FA jest WYMUSZONE. `false` to świadoma rezygnacja — unikaj.
|
||||
|
||||
### Jak działa dla użytkownika
|
||||
|
||||
1. Loguje się (email + hasło)
|
||||
2. Przy pierwszym logowaniu: przekierowanie na Setup TOTP (QR + sekret)
|
||||
3. Skanuje QR aplikacją (Google Authenticator, Authy, 1Password, Microsoft Auth)
|
||||
4. Wpisuje kod → 2FA aktywne
|
||||
5. Kolejne logowania: email + hasło + kod TOTP
|
||||
|
||||
### Reset 2FA (admin)
|
||||
|
||||
Admin może zresetować 2FA innego użytkownika (gdy zgubi telefon) — przez
|
||||
`adminManageAccess` w konfiguracji @clocklimited. Patrz jego dokumentacja.
|
||||
|
||||
### Dlaczego @clocklimited, nie inne
|
||||
|
||||
Wybrany, bo wrapuje ACCESS CONTROL (TOTP przed dostępem do danych) + forceSetup
|
||||
(wymuszenie dla wszystkich). Inne pluginy 2FA dla Payload gatują tylko nawigację
|
||||
/admin — user z hasłem może omijać przez REST/GraphQL/Bearer. @clocklimited chroni
|
||||
dostęp do danych, nie tylko UI.
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@intecion/ipal-kit",
|
||||
"version": "1.4.4",
|
||||
"version": "1.5.0",
|
||||
"description": "Intecion Payload Advanced Library — a Payload CMS 3 plugin: i18n, SEO, forms, consent, analytics, blog/archives.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -67,6 +67,7 @@
|
||||
"slugify": "^1.6.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@clocklimited/payload-2fa": "^3.0.0",
|
||||
"@payloadcms/next": "^3.88.0",
|
||||
"@payloadcms/plugin-form-builder": "^3.88.0",
|
||||
"@payloadcms/plugin-seo": "^3.88.0",
|
||||
|
||||
@@ -1,76 +1,143 @@
|
||||
/*
|
||||
* Starter stylesheet for a sitemap served via buildSitemapXml({ cssUrl }).
|
||||
* Uses type="text/css" on XML (W3C "Associating Style Sheets with XML") — NOT
|
||||
* XSLT, so no browser deprecation warning. Selectors are the XML tag names.
|
||||
* Universal, minimalist & elegant stylesheet for XML Sitemap.
|
||||
* Neutral palette with automatic dark and light mode support.
|
||||
* W3C standard: type="text/css" on XML (zero browser deprecation warnings).
|
||||
*
|
||||
* Copy to your project's /public/sitemap.css and adjust colors/spacing to taste.
|
||||
* Crawlers ignore this; it only affects the human-readable browser view.
|
||||
* Ships with @intecion/ipal-kit. Copy to /public/sitemap.css and adjust freely.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--bg: #fafafa;
|
||||
--card: #ffffff;
|
||||
--border: #e5e7eb;
|
||||
--border-hover: #d1d5db;
|
||||
--text-main: #111827;
|
||||
--text-secondary: #4b5563;
|
||||
--text-muted: #9ca3af;
|
||||
--url-color: #1e293b;
|
||||
--badge-bg: #f3f4f6;
|
||||
--badge-border: #e5e7eb;
|
||||
--badge-text: #4b5563;
|
||||
--accent: #f97316;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #090a0f;
|
||||
--card: #12131a;
|
||||
--border: #1e202e;
|
||||
--border-hover: #2e3247;
|
||||
--text-main: #f9fafb;
|
||||
--text-secondary: #9ca3af;
|
||||
--text-muted: #6b7280;
|
||||
--url-color: #f3f4f6;
|
||||
--badge-bg: #1a1c26;
|
||||
--badge-border: #282b3d;
|
||||
--badge-text: #9ca3af;
|
||||
--accent: #fb923c;
|
||||
}
|
||||
}
|
||||
|
||||
urlset {
|
||||
display: block;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #090d16;
|
||||
color: #f1f5f9;
|
||||
padding: 2rem 1.5rem;
|
||||
max-width: 1200px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Inter, Helvetica, Arial, sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--text-main);
|
||||
padding: 3rem 1.5rem;
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Each URL entry as a card. */
|
||||
/* Minimalist header */
|
||||
urlset::before {
|
||||
content: "XML Sitemap";
|
||||
display: block;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--text-main);
|
||||
padding-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
/* Brand note under the header — crafted by Intecion Group */
|
||||
urlset::after {
|
||||
content: "Intecion.com, Technology — engineered for modern digital experiences.";
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
padding-bottom: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* URL card */
|
||||
url {
|
||||
display: block;
|
||||
background: #111827;
|
||||
border: 1px solid #1e293b;
|
||||
background-color: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.65rem;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
/* The URL itself. */
|
||||
url:hover {
|
||||
border-color: var(--border-hover);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
/* URL address */
|
||||
loc {
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #f97316;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--url-color);
|
||||
word-break: break-all;
|
||||
line-height: 1.45;
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
/* Metadata line: lastmod / changefreq / priority, each with a label. */
|
||||
/* Metadata row */
|
||||
lastmod,
|
||||
changefreq,
|
||||
priority {
|
||||
display: inline-block;
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
margin-right: 1.5rem;
|
||||
font-size: 0.775rem;
|
||||
color: var(--text-secondary);
|
||||
margin-right: 1.25rem;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
lastmod::before {
|
||||
content: 'Ostatnia modyfikacja: ';
|
||||
color: #64748b;
|
||||
content: "Zaktualizowano: ";
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
changefreq::before {
|
||||
content: 'Częstotliwość: ';
|
||||
color: #64748b;
|
||||
content: "Częstotliwość: ";
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
priority::before {
|
||||
content: 'Priorytet: ';
|
||||
color: #64748b;
|
||||
content: "Priorytet: ";
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* hreflang alternates as small pills. */
|
||||
/* Alternate language pills */
|
||||
link {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
background: #1e293b;
|
||||
color: #38bdf8;
|
||||
border: 1px solid #334155;
|
||||
padding: 0.15rem 0.45rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
background-color: var(--badge-bg);
|
||||
border: 1px solid var(--badge-border);
|
||||
color: var(--badge-text);
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
margin: 0.4rem 0.35rem 0 0;
|
||||
margin-right: 0.3rem;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
+46
-7
@@ -49,6 +49,51 @@ export const ipalKit = (options: IpalOptions): Plugin => {
|
||||
|
||||
let config = { ...incomingConfig }
|
||||
|
||||
// --- custom admin route (e.g. '/its' instead of '/admin') ---
|
||||
// Sets config.routes.admin; the project must move its app/(payload)/<route>/
|
||||
// folder to match (plugin can't create files in the project's app/).
|
||||
if (options.adminRoute) {
|
||||
config.routes = { ...(config.routes ?? {}), admin: options.adminRoute }
|
||||
}
|
||||
|
||||
// --- enforced 2FA (TOTP) via @clocklimited/payload-2fa ---
|
||||
// Enforced by default (forceSetup) unless twoFactor is explicitly false. The
|
||||
// plugin is a PEER dependency imported dynamically — ipal-kit doesn't bundle
|
||||
// it, so projects that opt out (twoFactor: false) needn't install it, and the
|
||||
// import never runs under generate:importmap when 2FA is off. Wrapping access
|
||||
// control (not just admin UI) means TOTP gates data access — no API bypass.
|
||||
if (options.twoFactor !== false) {
|
||||
const tf = options.twoFactor
|
||||
if (!tf?.issuer) {
|
||||
throw new Error(
|
||||
'[ipal] twoFactor.issuer is required (name shown in the authenticator ' +
|
||||
'app). Pass twoFactor: { issuer: "Your Site" }, or twoFactor: false to ' +
|
||||
'opt out (discouraged).',
|
||||
)
|
||||
}
|
||||
try {
|
||||
// Dynamic specifier via a variable so TS doesn't try to resolve this
|
||||
// optional peer dependency at build time (it isn't in the plugin's own
|
||||
// node_modules). Avoids TS2307 without @ts-expect-error; the module
|
||||
// exists at runtime in projects that installed it.
|
||||
const pkg = '@clocklimited/payload-2fa'
|
||||
const { totpPlugin } = (await import(pkg)) as {
|
||||
totpPlugin: (opts: Record<string, unknown>) => Plugin
|
||||
}
|
||||
config = await totpPlugin({
|
||||
collection: tf.collectionSlug ?? 'users',
|
||||
forceSetup: true, // ENFORCED — every user must set up TOTP; no opt-out
|
||||
totp: { issuer: tf.issuer },
|
||||
})(config)
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
'[ipal] 2FA is enforced but @clocklimited/payload-2fa is not installed. ' +
|
||||
'Run: pnpm add @clocklimited/payload-2fa — or set twoFactor: false to ' +
|
||||
`opt out (discouraged). Original error: ${String(err)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- i18n ---
|
||||
config.localization = buildLocalizationConfig(options.i18n)
|
||||
|
||||
@@ -99,18 +144,12 @@ export const ipalKit = (options: IpalOptions): Plugin => {
|
||||
buildNotifications(),
|
||||
]
|
||||
|
||||
// --- endpoints ---
|
||||
// Test-email endpoint (admin-only): POST /api/ipal/test-email sends a probe
|
||||
// message through the currently selected transport, so the panel's "send
|
||||
// test" button can confirm delivery without leaving the admin UI.
|
||||
config.endpoints = [...(config.endpoints ?? []), testEmailEndpoint]
|
||||
|
||||
// --- hooks: onInit ---
|
||||
const incomingOnInit = config.onInit
|
||||
config.onInit = async (payload) => {
|
||||
if (incomingOnInit) {
|
||||
await incomingOnInit(payload)
|
||||
}
|
||||
if (incomingOnInit) {await incomingOnInit(payload)}
|
||||
payload.logger.info('[ipal] Plugin initialized.')
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,18 @@ export type IpalOptions = {
|
||||
*/
|
||||
access?: AccessOption
|
||||
|
||||
/**
|
||||
* Custom admin panel route, e.g. '/its' instead of the default '/admin'.
|
||||
* The plugin sets config.routes.admin. IMPORTANT: this alone isn't enough —
|
||||
* the project must ALSO move its panel folder to match:
|
||||
* app/(payload)/its/[[...segments]]/page.tsx (and the not-found). The plugin
|
||||
* can't create files in the project's app/. See docs/security.md.
|
||||
*
|
||||
* This is obscurity, not security: it hides the panel from dumb bots scanning
|
||||
* /admin, but real protection is strong auth + 2FA + rate limiting.
|
||||
*/
|
||||
adminRoute?: string
|
||||
|
||||
/**
|
||||
* Collections whose entries live under an archive page — blog posts, case
|
||||
* studies, anything with a listing. Adds an "archive page" assignment per
|
||||
@@ -55,4 +67,23 @@ export type IpalOptions = {
|
||||
|
||||
/** Additional fields injected into SiteSettings global */
|
||||
siteSettingsFields?: Field[]
|
||||
|
||||
/**
|
||||
* Two-factor authentication (TOTP), ENFORCED for every user. Wires
|
||||
* @clocklimited/payload-2fa with forceSetup — no per-project opt-out. Every
|
||||
* user must configure an authenticator app after login; TOTP is checked before
|
||||
* data access (not just the admin UI). Requires the peer dep installed and an
|
||||
* issuer name (shown in the authenticator app).
|
||||
*
|
||||
* Pass `false` ONLY for a project that genuinely can't use 2FA (rare, discouraged)
|
||||
* — default is enforced. See docs/security.md.
|
||||
*/
|
||||
twoFactor?:
|
||||
| {
|
||||
/** Auth collection slug. Defaults to 'users'. */
|
||||
collectionSlug?: string
|
||||
/** Name shown in the authenticator app (e.g. company/site name). */
|
||||
issuer: string
|
||||
}
|
||||
| false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user