feat: upgrade Payload CMS 3.69.0 → 3.76.1

Upgrade all 11 @payloadcms/* packages to 3.76.1, gaining fixes from
PRs #15404 (user.collection property for multi-tenant access control)
and #15499 (tenant selector uses beforeNav slot).

Fix afterLogin audit hook deadlock: payload.create() inside the hook
caused a transaction deadlock with PgBouncer in transaction mode under
Payload 3.76.1's stricter transaction handling. Changed to fire-and-forget
pattern to prevent login hangs.

Note: Next.js 15.5.9 peer dependency warning exists but build/runtime
work correctly. Consider upgrading Next.js to 16.x or downgrading to
15.4.11 in a follow-up.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2026-02-13 11:07:46 +00:00
parent 9f575963ed
commit 304e54f9e2
5 changed files with 661 additions and 305 deletions

View file

@ -28,16 +28,16 @@
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.71.2", "@anthropic-ai/sdk": "^0.71.2",
"@payloadcms/db-postgres": "3.69.0", "@payloadcms/db-postgres": "3.76.1",
"@payloadcms/next": "3.69.0", "@payloadcms/next": "3.76.1",
"@payloadcms/plugin-form-builder": "3.69.0", "@payloadcms/plugin-form-builder": "3.76.1",
"@payloadcms/plugin-multi-tenant": "3.69.0", "@payloadcms/plugin-multi-tenant": "3.76.1",
"@payloadcms/plugin-nested-docs": "3.69.0", "@payloadcms/plugin-nested-docs": "3.76.1",
"@payloadcms/plugin-redirects": "3.69.0", "@payloadcms/plugin-redirects": "3.76.1",
"@payloadcms/plugin-seo": "3.69.0", "@payloadcms/plugin-seo": "3.76.1",
"@payloadcms/richtext-lexical": "3.69.0", "@payloadcms/richtext-lexical": "3.76.1",
"@payloadcms/translations": "3.69.0", "@payloadcms/translations": "3.76.1",
"@payloadcms/ui": "3.69.0", "@payloadcms/ui": "3.76.1",
"@types/pdfkit": "^0.17.4", "@types/pdfkit": "^0.17.4",
"bullmq": "^5.65.1", "bullmq": "^5.65.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@ -49,7 +49,7 @@
"next": "15.5.9", "next": "15.5.9",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"nodemailer": "^7.0.11", "nodemailer": "^7.0.11",
"payload": "3.69.0", "payload": "3.76.1",
"payload-oapi": "^0.2.5", "payload-oapi": "^0.2.5",
"pdfkit": "^0.17.2", "pdfkit": "^0.17.2",
"react": "19.2.3", "react": "19.2.3",

File diff suppressed because it is too large Load diff

View file

@ -28,7 +28,7 @@ import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0
import { CommunityNavLinks as CommunityNavLinks_4431db66fbe96916dda5fdbfa979ee1e } from '@/components/admin/CommunityNavLinks' import { CommunityNavLinks as CommunityNavLinks_4431db66fbe96916dda5fdbfa979ee1e } from '@/components/admin/CommunityNavLinks'
import { TenantSelector as TenantSelector_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc' import { TenantSelector as TenantSelector_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
import { TenantSelectionProvider as TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc' import { TenantSelectionProvider as TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
import { CollectionCards as CollectionCards_ab83ff7e88da8d3530831f296ec4756a } from '@payloadcms/ui/rsc' import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
export const importMap = { export const importMap = {
"@payloadcms/plugin-multi-tenant/client#TenantField": TenantField_1d0591e3cf4f332c83a86da13a0de59a, "@payloadcms/plugin-multi-tenant/client#TenantField": TenantField_1d0591e3cf4f332c83a86da13a0de59a,
@ -61,5 +61,5 @@ export const importMap = {
"@/components/admin/CommunityNavLinks#CommunityNavLinks": CommunityNavLinks_4431db66fbe96916dda5fdbfa979ee1e, "@/components/admin/CommunityNavLinks#CommunityNavLinks": CommunityNavLinks_4431db66fbe96916dda5fdbfa979ee1e,
"@payloadcms/plugin-multi-tenant/rsc#TenantSelector": TenantSelector_d6d5f193a167989e2ee7d14202901e62, "@payloadcms/plugin-multi-tenant/rsc#TenantSelector": TenantSelector_d6d5f193a167989e2ee7d14202901e62,
"@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62, "@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62,
"@payloadcms/ui/rsc#CollectionCards": CollectionCards_ab83ff7e88da8d3530831f296ec4756a "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
} }

View file

@ -31,7 +31,12 @@ interface AuthUser {
export const auditAfterLogin: CollectionAfterLoginHook = async ({ user, req }) => { export const auditAfterLogin: CollectionAfterLoginHook = async ({ user, req }) => {
const typedUser = user as AuthUser const typedUser = user as AuthUser
await logLoginSuccess(req.payload, typedUser.id, typedUser.email, req) // Fire-and-forget: Audit-Log darf Login nicht blockieren
// In Payload 3.76.1+ verursacht ein awaited payload.create() innerhalb
// des afterLogin-Hooks einen Deadlock mit PgBouncer (Transaction-Mode)
logLoginSuccess(req.payload, typedUser.id, typedUser.email, req).catch((err) => {
console.error(`[Audit:Auth] Failed to log login for ${typedUser.email}:`, err)
})
console.log(`[Audit:Auth] Login success for ${typedUser.email}`) console.log(`[Audit:Auth] Login success for ${typedUser.email}`)

View file

@ -114,6 +114,7 @@ export interface Config {
'community-interactions': CommunityInteraction; 'community-interactions': CommunityInteraction;
'community-templates': CommunityTemplate; 'community-templates': CommunityTemplate;
'community-rules': CommunityRule; 'community-rules': CommunityRule;
'report-schedules': ReportSchedule;
'cookie-configurations': CookieConfiguration; 'cookie-configurations': CookieConfiguration;
'cookie-inventory': CookieInventory; 'cookie-inventory': CookieInventory;
'consent-logs': ConsentLog; 'consent-logs': ConsentLog;
@ -179,6 +180,7 @@ export interface Config {
'community-interactions': CommunityInteractionsSelect<false> | CommunityInteractionsSelect<true>; 'community-interactions': CommunityInteractionsSelect<false> | CommunityInteractionsSelect<true>;
'community-templates': CommunityTemplatesSelect<false> | CommunityTemplatesSelect<true>; 'community-templates': CommunityTemplatesSelect<false> | CommunityTemplatesSelect<true>;
'community-rules': CommunityRulesSelect<false> | CommunityRulesSelect<true>; 'community-rules': CommunityRulesSelect<false> | CommunityRulesSelect<true>;
'report-schedules': ReportSchedulesSelect<false> | ReportSchedulesSelect<true>;
'cookie-configurations': CookieConfigurationsSelect<false> | CookieConfigurationsSelect<true>; 'cookie-configurations': CookieConfigurationsSelect<false> | CookieConfigurationsSelect<true>;
'cookie-inventory': CookieInventorySelect<false> | CookieInventorySelect<true>; 'cookie-inventory': CookieInventorySelect<false> | CookieInventorySelect<true>;
'consent-logs': ConsentLogsSelect<false> | ConsentLogsSelect<true>; 'consent-logs': ConsentLogsSelect<false> | ConsentLogsSelect<true>;
@ -206,9 +208,7 @@ export interface Config {
'seo-settings': SeoSettingsSelect<false> | SeoSettingsSelect<true>; 'seo-settings': SeoSettingsSelect<false> | SeoSettingsSelect<true>;
}; };
locale: 'de' | 'en'; locale: 'de' | 'en';
user: User & { user: User;
collection: 'users';
};
jobs: { jobs: {
tasks: unknown; tasks: unknown;
workflows: unknown; workflows: unknown;
@ -277,6 +277,7 @@ export interface User {
}[] }[]
| null; | null;
password?: string | null; password?: string | null;
collection: 'users';
} }
/** /**
* YouTube-Kanäle und ihre Konfiguration * YouTube-Kanäle und ihre Konfiguration
@ -6551,6 +6552,10 @@ export interface YtNotification {
| 'video_published' | 'video_published'
| 'comment' | 'comment'
| 'mention' | 'mention'
| 'token_expiring'
| 'token_expired'
| 'token_refresh_failed'
| 'token_refreshed'
| 'system'; | 'system';
title: string; title: string;
message?: string | null; message?: string | null;
@ -6560,12 +6565,118 @@ export interface YtNotification {
link?: string | null; link?: string | null;
relatedVideo?: (number | null) | YoutubeContent; relatedVideo?: (number | null) | YoutubeContent;
relatedTask?: (number | null) | YtTask; relatedTask?: (number | null) | YtTask;
/**
* Verknüpfter Social Account (für Token-Benachrichtigungen)
*/
relatedAccount?: (number | null) | SocialAccount;
read?: boolean | null; read?: boolean | null;
readAt?: string | null; readAt?: string | null;
emailSent?: boolean | null; emailSent?: boolean | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "social-accounts".
*/
export interface SocialAccount {
id: number;
platform: number | SocialPlatform;
/**
* Für Zuordnung zu Brand/Kanal
*/
linkedChannel?: (number | null) | YoutubeChannel;
displayName: string;
accountHandle?: string | null;
/**
* YouTube Channel ID, LinkedIn URN, etc.
*/
externalId?: string | null;
accountUrl?: string | null;
isActive?: boolean | null;
/**
* Sensible Daten nur für Super-Admins sichtbar
*/
credentials?: {
/**
* OAuth Access Token
*/
accessToken?: string | null;
refreshToken?: string | null;
tokenExpiresAt?: string | null;
/**
* Für API-Key basierte Auth
*/
apiKey?: string | null;
};
stats?: {
followers?: number | null;
totalPosts?: number | null;
lastSyncedAt?: string | null;
};
syncSettings?: {
autoSyncEnabled?: boolean | null;
syncIntervalMinutes?: number | null;
syncComments?: boolean | null;
/**
* Nicht alle Plattformen unterstützen DM-API
*/
syncDMs?: boolean | null;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "social-platforms".
*/
export interface SocialPlatform {
id: number;
name: string;
slug: string;
icon?: string | null;
color?: string | null;
isActive?: boolean | null;
apiStatus?: ('connected' | 'limited' | 'disconnected' | 'development') | null;
apiConfig?: {
apiType?: ('youtube_v3' | 'linkedin' | 'instagram_graph' | 'facebook_graph' | 'meta_graph' | 'custom') | null;
baseUrl?: string | null;
authType?: ('oauth2' | 'api_key' | 'bearer') | null;
/**
* Relativer API-Pfad für OAuth-Initiation (z.B. /api/youtube/auth)
*/
oauthEndpoint?: string | null;
scopes?:
| {
scope?: string | null;
id?: string | null;
}[]
| null;
/**
* Wie lange ist der Access Token gültig? (YouTube: unbegrenzt mit Refresh, Meta: 60 Tage)
*/
tokenValidityDays?: number | null;
};
interactionTypes?:
| {
type: string;
label: string;
icon?: string | null;
canReply?: boolean | null;
id?: string | null;
}[]
| null;
rateLimits?: {
requestsPerMinute?: number | null;
requestsPerDay?: number | null;
/**
* YouTube: 10.000/Tag
*/
quotaUnitsPerDay?: number | null;
};
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "yt-monthly-goals". * via the `definition` "yt-monthly-goals".
@ -6661,100 +6772,6 @@ export interface YtChecklistTemplate {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "social-platforms".
*/
export interface SocialPlatform {
id: number;
name: string;
slug: string;
icon?: string | null;
color?: string | null;
isActive?: boolean | null;
apiStatus?: ('connected' | 'limited' | 'disconnected' | 'development') | null;
apiConfig?: {
apiType?: ('youtube_v3' | 'linkedin' | 'instagram_graph' | 'facebook_graph' | 'custom') | null;
baseUrl?: string | null;
authType?: ('oauth2' | 'api_key' | 'bearer') | null;
scopes?:
| {
scope?: string | null;
id?: string | null;
}[]
| null;
};
interactionTypes?:
| {
type: string;
label: string;
icon?: string | null;
canReply?: boolean | null;
id?: string | null;
}[]
| null;
rateLimits?: {
requestsPerMinute?: number | null;
requestsPerDay?: number | null;
/**
* YouTube: 10.000/Tag
*/
quotaUnitsPerDay?: number | null;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "social-accounts".
*/
export interface SocialAccount {
id: number;
platform: number | SocialPlatform;
/**
* Für Zuordnung zu Brand/Kanal
*/
linkedChannel?: (number | null) | YoutubeChannel;
displayName: string;
accountHandle?: string | null;
/**
* YouTube Channel ID, LinkedIn URN, etc.
*/
externalId?: string | null;
accountUrl?: string | null;
isActive?: boolean | null;
/**
* Sensible Daten nur für Super-Admins sichtbar
*/
credentials?: {
/**
* OAuth Access Token
*/
accessToken?: string | null;
refreshToken?: string | null;
tokenExpiresAt?: string | null;
/**
* Für API-Key basierte Auth
*/
apiKey?: string | null;
};
stats?: {
followers?: number | null;
totalPosts?: number | null;
lastSyncedAt?: string | null;
};
syncSettings?: {
autoSyncEnabled?: boolean | null;
syncIntervalMinutes?: number | null;
syncComments?: boolean | null;
/**
* Nicht alle Plattformen unterstützen DM-API
*/
syncDMs?: boolean | null;
};
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "community-interactions". * via the `definition` "community-interactions".
@ -6980,6 +6997,64 @@ export interface CommunityRule {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* Automatische Community-Reports per E-Mail
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "report-schedules".
*/
export interface ReportSchedule {
id: number;
/**
* Interner Name für diesen Report-Zeitplan
*/
name: string;
/**
* Deaktivierte Reports werden nicht automatisch versendet
*/
enabled?: boolean | null;
frequency: 'daily' | 'weekly' | 'monthly';
/**
* Für wöchentliche Reports
*/
dayOfWeek?: ('monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday') | null;
/**
* Für monatliche Reports (1-28)
*/
dayOfMonth?: number | null;
/**
* Format: HH:MM (24-Stunden)
*/
time: string;
timezone: 'Europe/Berlin' | 'Europe/London' | 'America/New_York' | 'America/Los_Angeles' | 'Asia/Tokyo' | 'UTC';
/**
* Art der Daten im Report
*/
reportType: 'overview' | 'sentiment_analysis' | 'response_metrics' | 'content_performance' | 'full_report';
/**
* Leer = alle aktiven Kanäle. Oder spezifische Kanäle auswählen.
*/
channels?: (number | SocialAccount)[] | null;
/**
* Wie viele Tage zurück sollen analysiert werden?
*/
periodDays?: number | null;
format: 'pdf' | 'excel' | 'html_email';
recipients: {
email: string;
/**
* Optional
*/
name?: string | null;
id?: string | null;
}[];
lastSentAt?: string | null;
nextScheduledAt?: string | null;
sendCount?: number | null;
lastError?: string | null;
updatedAt: string;
createdAt: string;
}
/** /**
* Cookie-Banner Konfiguration pro Tenant * Cookie-Banner Konfiguration pro Tenant
* *
@ -7697,6 +7772,10 @@ export interface PayloadLockedDocument {
relationTo: 'community-rules'; relationTo: 'community-rules';
value: number | CommunityRule; value: number | CommunityRule;
} | null) } | null)
| ({
relationTo: 'report-schedules';
value: number | ReportSchedule;
} | null)
| ({ | ({
relationTo: 'cookie-configurations'; relationTo: 'cookie-configurations';
value: number | CookieConfiguration; value: number | CookieConfiguration;
@ -11599,6 +11678,7 @@ export interface YtNotificationsSelect<T extends boolean = true> {
link?: T; link?: T;
relatedVideo?: T; relatedVideo?: T;
relatedTask?: T; relatedTask?: T;
relatedAccount?: T;
read?: T; read?: T;
readAt?: T; readAt?: T;
emailSent?: T; emailSent?: T;
@ -11770,12 +11850,14 @@ export interface SocialPlatformsSelect<T extends boolean = true> {
apiType?: T; apiType?: T;
baseUrl?: T; baseUrl?: T;
authType?: T; authType?: T;
oauthEndpoint?: T;
scopes?: scopes?:
| T | T
| { | {
scope?: T; scope?: T;
id?: T; id?: T;
}; };
tokenValidityDays?: T;
}; };
interactionTypes?: interactionTypes?:
| T | T
@ -11991,6 +12073,36 @@ export interface CommunityRulesSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "report-schedules_select".
*/
export interface ReportSchedulesSelect<T extends boolean = true> {
name?: T;
enabled?: T;
frequency?: T;
dayOfWeek?: T;
dayOfMonth?: T;
time?: T;
timezone?: T;
reportType?: T;
channels?: T;
periodDays?: T;
format?: T;
recipients?:
| T
| {
email?: T;
name?: T;
id?: T;
};
lastSentAt?: T;
nextScheduledAt?: T;
sendCount?: T;
lastError?: T;
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "cookie-configurations_select". * via the `definition` "cookie-configurations_select".