Upgrade system finished
This commit is contained in:
parent
a71ba20176
commit
bb1935027d
11
.env.example
11
.env.example
@ -147,6 +147,17 @@ VIDEO_SCHEDULE_NOTIFICATION_ENABLED=true
|
|||||||
# Preview Links (Feb 2026)
|
# Preview Links (Feb 2026)
|
||||||
VIDEO_PREVIEW_LINK_EXPIRY_HOURS=24
|
VIDEO_PREVIEW_LINK_EXPIRY_HOURS=24
|
||||||
|
|
||||||
|
# --- Container Registry ---
|
||||||
|
# Gitea registry for pre-built production images.
|
||||||
|
# Set IMAGE_TAG to a commit SHA (or 'latest') to pull pre-built images instead of building from source.
|
||||||
|
# Leave IMAGE_TAG blank/unset (defaults to 'local') to build locally from source.
|
||||||
|
GITEA_REGISTRY=gitea.bnkops.com/admin
|
||||||
|
IMAGE_TAG=
|
||||||
|
# Credentials used by the registry status API endpoint (GET /api/registry/status)
|
||||||
|
# For docker push/pull, run: docker login gitea.bnkops.com
|
||||||
|
GITEA_REGISTRY_USER=admin
|
||||||
|
GITEA_REGISTRY_PASS=
|
||||||
|
|
||||||
# --- Gitea ---
|
# --- Gitea ---
|
||||||
GITEA_URL=http://gitea-changemaker:3000
|
GITEA_URL=http://gitea-changemaker:3000
|
||||||
GITEA_PORT=3030
|
GITEA_PORT=3030
|
||||||
|
|||||||
@ -192,3 +192,251 @@
|
|||||||
at async Object.fetchMe (http://localhost:3002/src/stores/auth.store.ts:101:28)
|
at async Object.fetchMe (http://localhost:3002/src/stores/auth.store.ts:101:28)
|
||||||
at async hydrate (http://localhost:3002/src/stores/auth.store.ts:118:11) @ http://localhost:3002/src/stores/auth.store.ts:105
|
at async hydrate (http://localhost:3002/src/stores/auth.store.ts:118:11) @ http://localhost:3002/src/stores/auth.store.ts:105
|
||||||
[ 1685344ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/payments/plans:0
|
[ 1685344ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3002/api/payments/plans:0
|
||||||
|
[758179964ms] [ERROR] WebSocket connection to 'ws://localhost:3002/' failed: Connection closed before receiving a handshake response @ http://localhost:3002/@vite/client:1034
|
||||||
|
[758181440ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MyCampaignsPage.tsx:0
|
||||||
|
[758181440ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ResponseWallPage.tsx:0
|
||||||
|
[758181440ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MapPage.tsx:0
|
||||||
|
[758181440ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ShiftsPage.tsx:0
|
||||||
|
[758181440ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MediaGalleryPage.tsx:0
|
||||||
|
[758181441ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ShortsPage.tsx:0
|
||||||
|
[758181441ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MediaViewerPage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PlaylistBrowsePage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PlaylistViewerPage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/media/PlaylistManagementPage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MyStatsPage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MySettingsPage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/VolunteerChatPage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PricingPage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ShopPage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ProductDetailPage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PlanDetailPage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/DonatePage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/DonationPagesListPage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PaymentSuccessPage.tsx:0
|
||||||
|
[758181442ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/MyActivityPage.tsx:0
|
||||||
|
[758181443ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/VolunteerShiftsPage.tsx:0
|
||||||
|
[758181443ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/MyRoutesPage.tsx:0
|
||||||
|
[758181443ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/VolunteerMapPage.tsx:0
|
||||||
|
[758181443ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/FriendsPage.tsx:0
|
||||||
|
[758181443ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/SocialProfilePage.tsx:0
|
||||||
|
[758181443ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/NotificationsPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/SocialFeedPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/DiscoverPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/GroupDetailPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/AchievementsPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/types/api.ts:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/roles.ts:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/QuickJoinPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/VerifyEmailPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/ResetPasswordPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsDashboardPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsContactsPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsCampaignsPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsConversationsPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsTemplatesPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/sms/SmsSetupPage.tsx:0
|
||||||
|
[758181444ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/PeoplePage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/ContactProfilePage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/SocialDashboardPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/SocialGraphPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/SocialModerationPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/ReferralAdminPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/SpotlightAdminPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/social/ChallengesAdminPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/influence/ImpactStoriesPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/ReferralsPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/ChallengesPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/ChallengeDetailPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/WallOfFamePage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/MeetingJoinPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/MeetingPlannerPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/MeetingAgendaPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/ActionItemsPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/SchedulingPollPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/PollsListPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/JitsiAuthPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/SchedulingCalendarPage.tsx:0
|
||||||
|
[758181445ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/AdminCalendarViewPage.tsx:0
|
||||||
|
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/events/TicketedEventsPage.tsx:0
|
||||||
|
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/events/EventDetailPage.tsx:0
|
||||||
|
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/events/CheckInScannerPage.tsx:0
|
||||||
|
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/TicketedEventDetailPage.tsx:0
|
||||||
|
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/public/TicketConfirmationPage.tsx:0
|
||||||
|
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/MyTicketsPage.tsx:0
|
||||||
|
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/MyCalendarPage.tsx:0
|
||||||
|
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/SharedCalendarsPage.tsx:0
|
||||||
|
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/SharedCalendarViewPage.tsx:0
|
||||||
|
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/volunteer/FriendCalendarPage.tsx:0
|
||||||
|
[758181446ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/NotFoundPage.tsx:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/command-palette/CommandPalette.tsx:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/lib/api.ts:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/VolunteerFooterNav.tsx:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/PublicNavBar.tsx:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useSSE.ts:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useLocalStorage.ts:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/lib/service-url.ts:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/lib/nav-defaults.ts:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/stores/command-palette.store.ts:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/stores/favorites.store.ts:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/menu-items.ts:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/chat/RocketChatWidget.tsx:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/MediaSidebar.tsx:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/MediaBottomNav.tsx:0
|
||||||
|
[758181447ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/ChatNotificationToast.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/chatbar/ChatBarContext.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/chatbar/ChatBar.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useChatNotifications.ts:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/color.ts:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/AuthModal.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/public/NewsletterSignup.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/pages/CampaignEmailsDrawer.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/canvass/ExportContactsModal.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/QrCodeModal.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/VideoPickerModal.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/email-templates/TestEmailModal.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/email-templates/VersionHistoryDrawer.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/email-templates/EmailTemplateEditor.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/map/CutEditorMap.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/SystemGauges.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/MiniDonutChart.tsx:0
|
||||||
|
[758181448ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/RequestTrafficChart.tsx:0
|
||||||
|
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/LatencyBandsChart.tsx:0
|
||||||
|
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/ContainerPopover.tsx:0
|
||||||
|
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/ContainerMemoryChart.tsx:0
|
||||||
|
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/ActivityFeedCard.tsx:0
|
||||||
|
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/TodayEventsCard.tsx:0
|
||||||
|
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/ChatNotifierCard.tsx:0
|
||||||
|
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/TopVideosCard.tsx:0
|
||||||
|
[758181449ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/RecentCommentsCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/DocsAnalyticsCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/UpcomingShiftsCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/MyActionItemsCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/CampaignEffectivenessCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/RecentSignupsCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/NewsletterStatsCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/DonationSummaryCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/SystemAlertsCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/GiteaActivityCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/VaultwardenAdoptionCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/dashboard/UpcomingMeetingsCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/canvass/CutCampaignAnalyticsCard.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/shifts/EditModeModal.tsx:0
|
||||||
|
[758181450ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/shifts/ShiftsCalendar.tsx:0
|
||||||
|
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/map/AdminMapView.tsx:0
|
||||||
|
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/map/AreaImportWizard.tsx:0
|
||||||
|
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/types/canvass.ts:0
|
||||||
|
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/canvass/AdminLiveMap.tsx:0
|
||||||
|
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/canvass/HistoricalRoutesDrawer.tsx:0
|
||||||
|
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/canvass/CanvassTrendsCard.tsx:0
|
||||||
|
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useMkDocsBuild.ts:0
|
||||||
|
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/landing-pages/LandingPageEditor.tsx:0
|
||||||
|
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useDocsEditor.ts:0
|
||||||
|
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/docs/MobileDocsEditor.tsx:0
|
||||||
|
[758181451ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/PhotoPickerModal.tsx:0
|
||||||
|
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/PhotoInsertModal.tsx:0
|
||||||
|
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/videoCardHtml.ts:0
|
||||||
|
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/photoCardHtml.ts:0
|
||||||
|
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/payments/DonateInsertModal.tsx:0
|
||||||
|
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/payments/ProductInsertModal.tsx:0
|
||||||
|
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/AdPickerModal.tsx:0
|
||||||
|
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/scheduling/PollInsertModal.tsx:0
|
||||||
|
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useDocsCollaboration.ts:0
|
||||||
|
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/docs/CollaboratorAvatars.tsx:0
|
||||||
|
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/utils/wikiLinkCompletion.ts:0
|
||||||
|
[758181452ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/docs/WikiLinkPickerModal.tsx:0
|
||||||
|
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/observability/ServiceStatusCard.tsx:0
|
||||||
|
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/observability/MetricsGrid.tsx:0
|
||||||
|
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/observability/AlertsTable.tsx:0
|
||||||
|
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/observability/IframeErrorBoundary.tsx:0
|
||||||
|
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/lib/media-api.ts:0
|
||||||
|
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/standalone/browser/standalone-tokens.css:0
|
||||||
|
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/aria/aria.css:0
|
||||||
|
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/widget/codeEditor/editor.css:0
|
||||||
|
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/scrollbar/media/scrollbars.css:0
|
||||||
|
[758181453ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/blockDecorations/blockDecorations.css:0
|
||||||
|
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.css:0
|
||||||
|
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/decorations/decorations.css:0
|
||||||
|
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/glyphMargin/glyphMargin.css:0
|
||||||
|
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/indentGuides/indentGuides.css:0
|
||||||
|
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css:0
|
||||||
|
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/mouseCursor/mouseCursor.css:0
|
||||||
|
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/viewLines/viewLines.css:0
|
||||||
|
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/linesDecorations/linesDecorations.css:0
|
||||||
|
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/margin/margin.css:0
|
||||||
|
[758181454ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/marginDecorations/marginDecorations.css:0
|
||||||
|
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/minimap/minimap.css:0
|
||||||
|
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.css:0
|
||||||
|
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/rulers/rulers.css:0
|
||||||
|
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.css:0
|
||||||
|
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/selections/selections.css:0
|
||||||
|
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/viewCursors/viewCursors.css:0
|
||||||
|
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/whitespace/whitespace.css:0
|
||||||
|
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/gpu/css/media/decorationCssRuleExtractor.css:0
|
||||||
|
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.css:0
|
||||||
|
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/controller/editContext/native/nativeEditContext.css:0
|
||||||
|
[758181455ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/gpuMark/gpuMark.css:0
|
||||||
|
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/platform/hover/browser/hover.css:0
|
||||||
|
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/hover/hoverWidget.css:0
|
||||||
|
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/contextview/contextview.css:0
|
||||||
|
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/selectBox/selectBox.css:0
|
||||||
|
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/list/list.css:0
|
||||||
|
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/dnd/dnd.css:0
|
||||||
|
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/selectBox/selectBoxCustom.css:0
|
||||||
|
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/actionbar/actionbar.css:0
|
||||||
|
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/dropdown/dropdown.css:0
|
||||||
|
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/platform/actions/browser/menuEntryActionViewItem.css:0
|
||||||
|
[758181456ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/standalone/browser/quickInput/standaloneQuickInput.css:0
|
||||||
|
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/toggle/toggle.css:0
|
||||||
|
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/platform/quickinput/browser/media/quickInput.css:0
|
||||||
|
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/button/button.css:0
|
||||||
|
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/countBadge/countBadge.css:0
|
||||||
|
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/progressbar/progressbar.css:0
|
||||||
|
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/inputbox/inputBox.css:0
|
||||||
|
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/findinput/findInput.css:0
|
||||||
|
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/iconLabel/iconlabel.css:0
|
||||||
|
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/keybindingLabel/keybindingLabel.css:0
|
||||||
|
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/tree/media/tree.css:0
|
||||||
|
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/sash/sash.css:0
|
||||||
|
[758181457ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/splitview/splitview.css:0
|
||||||
|
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/table/table.css:0
|
||||||
|
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.css:0
|
||||||
|
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/base/browser/ui/toolbar/toolbar.css:0
|
||||||
|
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/widget/diffEditor/style.css:0
|
||||||
|
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/widget/markdownRenderer/browser/renderedMarkdown.css:0
|
||||||
|
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/node_modules/monaco-editor/esm/vs/editor/browser/widget/multiDiffEditor/style.css:0
|
||||||
|
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useDebounce.ts:0
|
||||||
|
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/VideoCard.tsx:0
|
||||||
|
[758181458ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/PhotoCard.tsx:0
|
||||||
|
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/AlbumCard.tsx:0
|
||||||
|
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/BulkActionsBar.tsx:0
|
||||||
|
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/PublishModal.tsx:0
|
||||||
|
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/DeleteConfirmModal.tsx:0
|
||||||
|
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/UploadVideoDrawer.tsx:0
|
||||||
|
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/UploadPhotoDrawer.tsx:0
|
||||||
|
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/VideoViewerModal.tsx:0
|
||||||
|
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/PhotoViewerModal.tsx:0
|
||||||
|
[758181459ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/EditPhotoModal.tsx:0
|
||||||
|
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/AlbumDetailDrawer.tsx:0
|
||||||
|
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/CreateAlbumModal.tsx:0
|
||||||
|
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/QuickAnalyticsModal.tsx:0
|
||||||
|
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/SchedulePublishModal.tsx:0
|
||||||
|
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/ScheduleCalendarDrawer.tsx:0
|
||||||
|
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/EditVideoModal.tsx:0
|
||||||
|
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/FetchVideosDrawer.tsx:0
|
||||||
|
[758181460ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/AddToPlaylistModal.tsx:0
|
||||||
|
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/BulkAddToPlaylistModal.tsx:0
|
||||||
|
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/BulkAccessLevelModal.tsx:0
|
||||||
|
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/types/gallery-ads.ts:0
|
||||||
|
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/GalleryAdCard.tsx:0
|
||||||
|
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/VideoPlayer.tsx:0
|
||||||
|
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/media/AdvancedVideoPlayer.tsx:0
|
||||||
|
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/payments/DonationWidget.tsx:0
|
||||||
|
[758181462ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/payments/PricingWidget.tsx:0
|
||||||
|
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/payments/ProductWidget.tsx:0
|
||||||
|
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/influence/CampaignFormWidget.tsx:0
|
||||||
|
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/scheduling/SchedulingPollWidget.tsx:0
|
||||||
|
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/useDocumentTitle.ts:0
|
||||||
|
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/hooks/usePageAds.ts:0
|
||||||
|
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/AdBanner.tsx:0
|
||||||
|
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/calendar/UnifiedCalendar.tsx:0
|
||||||
|
[758181463ms] [ERROR] Failed to load resource: net::ERR_NETWORK_CHANGED @ http://localhost:3002/src/components/public/ShiftSignupModal.tsx:0
|
||||||
|
|||||||
288
DEV_WORKFLOW.md
Normal file
288
DEV_WORKFLOW.md
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
# Development & Release Workflow
|
||||||
|
|
||||||
|
How code changes move from development to production deployments across all installation methods.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
There are **three ways** Changemaker Lite gets deployed:
|
||||||
|
|
||||||
|
| Method | Who uses it | Images from | Compose file |
|
||||||
|
|--------|------------|-------------|--------------|
|
||||||
|
| **Source install** | Developers, contributors | Built locally from source | `docker-compose.yml` |
|
||||||
|
| **Release install** | Production servers, evaluators | Gitea registry (pre-built) | `docker-compose.prod.yml` (ships as `docker-compose.yml` in tarball) |
|
||||||
|
| **CCP provisioned** | Fleet operators (Control Panel) | Gitea registry (pre-built) | Rendered from `templates/docker-compose.yml.hbs` |
|
||||||
|
|
||||||
|
All three methods share the same Gitea container registry at `gitea.bnkops.com/admin`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ DEVELOPMENT (your machine) │
|
||||||
|
│ │
|
||||||
|
│ Edit code → docker compose up -d → test locally │
|
||||||
|
│ Uses: docker-compose.yml (build: blocks + ./api:/app mounts) │
|
||||||
|
└──────────────────┬───────────────────────────────────────────────┘
|
||||||
|
│ git push
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ BUILD & PUBLISH │
|
||||||
|
│ │
|
||||||
|
│ Step 1: ./scripts/build-and-push.sh │
|
||||||
|
│ Builds 4 production images, pushes to Gitea registry │
|
||||||
|
│ (api, admin, media-api, nginx) tagged :SHA + :latest │
|
||||||
|
│ │
|
||||||
|
│ Step 2: ./scripts/mirror-images.sh (run once/rarely) │
|
||||||
|
│ Mirrors 36 third-party images to Gitea registry │
|
||||||
|
│ (postgres, redis, nocodb, jitsi, grafana, etc.) │
|
||||||
|
│ │
|
||||||
|
│ Step 3: ./scripts/build-release.sh --tag vX.Y.Z --upload │
|
||||||
|
│ Packages runtime files into ~9MB tarball, uploads to │
|
||||||
|
│ Gitea Releases │
|
||||||
|
└──────────────────┬───────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────┴───────────┐
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────┐ ┌──────────────────┐
|
||||||
|
│ RELEASE INSTALL │ │ CCP PROVISIONED │
|
||||||
|
│ │ │ │
|
||||||
|
│ curl installer │ │ Control Panel │
|
||||||
|
│ or manual tarball│ │ creates instance │
|
||||||
|
│ → config.sh │ │ via web UI │
|
||||||
|
│ → docker compose │ │ → renders config │
|
||||||
|
│ up -d │ │ → docker compose │
|
||||||
|
│ │ │ up -d │
|
||||||
|
└─────────────────┘ └──────────────────┘
|
||||||
|
│ │
|
||||||
|
└───────────┬───────────┘
|
||||||
|
▼
|
||||||
|
All images pulled from
|
||||||
|
gitea.bnkops.com/admin
|
||||||
|
(zero external dependencies)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step-by-Step
|
||||||
|
|
||||||
|
### 1. Local Development
|
||||||
|
|
||||||
|
Standard Docker Compose workflow with hot-reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start core services
|
||||||
|
docker compose up -d v2-postgres redis api admin
|
||||||
|
|
||||||
|
# API logs (watch for errors)
|
||||||
|
docker compose logs -f api
|
||||||
|
|
||||||
|
# Run with media API
|
||||||
|
docker compose up -d media-api
|
||||||
|
|
||||||
|
# Run with monitoring stack
|
||||||
|
docker compose --profile monitoring up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key:** `docker-compose.yml` uses `build:` blocks to compile TypeScript from source and mounts `./api:/app` for live code changes. This is the only compose file that builds from source.
|
||||||
|
|
||||||
|
### 2. Build & Push Production Images
|
||||||
|
|
||||||
|
After code changes are tested locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build production images and push to Gitea registry
|
||||||
|
./scripts/build-and-push.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds **4 services** with multi-stage Dockerfiles (production target, no dev dependencies), tags each image with `:SHA` and `:latest`, and pushes to `gitea.bnkops.com/admin/changemaker-{service}`:
|
||||||
|
|
||||||
|
| Service | Dockerfile | What it produces |
|
||||||
|
|---------|-----------|-----------------|
|
||||||
|
| `api` | `api/Dockerfile` | Express + Prisma (compiled JS, no TS) |
|
||||||
|
| `admin` | `admin/Dockerfile` | Nginx serving React build output |
|
||||||
|
| `media-api` | `api/Dockerfile.media` | Fastify + FFmpeg (compiled JS) |
|
||||||
|
| `nginx` | `nginx/Dockerfile` | Nginx with `envsubst` domain templating |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build specific services only
|
||||||
|
./scripts/build-and-push.sh --services api,admin
|
||||||
|
|
||||||
|
# Build without pushing (verify first)
|
||||||
|
./scripts/build-and-push.sh --no-push
|
||||||
|
|
||||||
|
# Include code-server (~9GB, only when Dockerfile changes)
|
||||||
|
./scripts/build-and-push.sh --include-code-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Mirror Third-Party Images (Run Once / On Version Bumps)
|
||||||
|
|
||||||
|
Copies all third-party Docker images used by the platform to the Gitea registry, so deployments never depend on Docker Hub, GHCR, LSCR, or GCR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mirror all 36 images (core + platform + comms + monitoring)
|
||||||
|
./scripts/mirror-images.sh
|
||||||
|
|
||||||
|
# Mirror only essential infrastructure (postgres, redis, alpine)
|
||||||
|
./scripts/mirror-images.sh --core-only
|
||||||
|
|
||||||
|
# Preview without executing
|
||||||
|
./scripts/mirror-images.sh --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to re-run:** Only when upgrading a third-party image version. The script has explicit version pins — update the version in `mirror-images.sh`, then re-run.
|
||||||
|
|
||||||
|
Images are organized into 4 groups:
|
||||||
|
|
||||||
|
| Group | Count | Examples |
|
||||||
|
|-------|-------|---------|
|
||||||
|
| Core Infrastructure | 5 | postgres:16-alpine, redis:7-alpine, alpine:3 |
|
||||||
|
| Platform Services | 16 | nocodb, listmonk, gitea, n8n, vaultwarden, nginx, code-server |
|
||||||
|
| Communication | 8 | rocket.chat, mongo, nats, gancio, jitsi (4 containers) |
|
||||||
|
| Monitoring | 7 | prometheus, grafana, alertmanager, cadvisor, exporters, gotify |
|
||||||
|
|
||||||
|
### 4. Build Release Tarball
|
||||||
|
|
||||||
|
Packages only runtime files (~9 MB) — no source code, no node_modules:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build tarball
|
||||||
|
./scripts/build-release.sh --tag v2.2.0
|
||||||
|
|
||||||
|
# Build and upload to Gitea Releases
|
||||||
|
./scripts/build-release.sh --tag v2.2.0 --upload
|
||||||
|
|
||||||
|
# Preview contents without creating tarball
|
||||||
|
./scripts/build-release.sh --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
The tarball contains:
|
||||||
|
- `docker-compose.yml` (copy of `docker-compose.prod.yml` — image-only, no build blocks)
|
||||||
|
- `.env.example`, `config.sh` (configuration wizard)
|
||||||
|
- `scripts/` (init scripts, backup, upgrade, systemd units)
|
||||||
|
- `configs/` (prometheus, grafana, alertmanager, homepage, pangolin)
|
||||||
|
- `nginx/conf.d/` (templates for reference)
|
||||||
|
- `mkdocs/` (starter documentation)
|
||||||
|
- Empty data directories
|
||||||
|
|
||||||
|
### 5. Deploying
|
||||||
|
|
||||||
|
#### New Release Install (End Users)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# One-liner
|
||||||
|
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
|
||||||
|
|
||||||
|
# Or manual
|
||||||
|
curl -LO https://gitea.bnkops.com/admin/changemaker.lite/releases/latest/download/changemaker-lite-latest.tar.gz
|
||||||
|
tar xzf changemaker-lite-latest.tar.gz
|
||||||
|
cd changemaker-lite
|
||||||
|
bash config.sh
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
All images (custom + third-party) pull from `gitea.bnkops.com/admin`. No external registry access needed.
|
||||||
|
|
||||||
|
#### New CCP Instance (Fleet Operators)
|
||||||
|
|
||||||
|
The Control Panel provisions instances via its web UI:
|
||||||
|
|
||||||
|
1. Operator fills in the Create Instance wizard (domain, features, email, tunnel)
|
||||||
|
2. CCP copies source files, renders templates (Handlebars), generates secrets
|
||||||
|
3. With `USE_REGISTRY_IMAGES=true` (default): pulls pre-built images from Gitea (~2 min)
|
||||||
|
4. With `USE_REGISTRY_IMAGES=false`: builds from source (~10+ min)
|
||||||
|
5. Starts infrastructure → runs migrations → starts all services
|
||||||
|
|
||||||
|
CCP registry settings (in `changemaker-control-panel/.env`):
|
||||||
|
```bash
|
||||||
|
GITEA_REGISTRY=gitea.bnkops.com/admin # Registry URL for all images
|
||||||
|
USE_REGISTRY_IMAGES=true # true = pull pre-built, false = build from source
|
||||||
|
IMAGE_TAG=latest # Tag for custom images (api, admin, media-api)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Upgrading Existing Installations
|
||||||
|
|
||||||
|
#### Source Installs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/upgrade.sh # Standard: git pull + rebuild from source
|
||||||
|
./scripts/upgrade.sh --use-registry # Fast: pull pre-built images instead of rebuilding
|
||||||
|
./scripts/upgrade.sh --dry-run # Preview changes
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Release Installs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/upgrade.sh # Auto-detects release mode, downloads latest tarball
|
||||||
|
```
|
||||||
|
|
||||||
|
Release installs are detected by the presence of a `VERSION` file and absence of `.git/`. The upgrade script automatically downloads the latest tarball from Gitea instead of running `git pull`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Image Naming Conventions
|
||||||
|
|
||||||
|
All images live under `gitea.bnkops.com/admin/`:
|
||||||
|
|
||||||
|
| Type | Naming Pattern | Example |
|
||||||
|
|------|---------------|---------|
|
||||||
|
| Custom services | `changemaker-{service}:{sha\|latest}` | `changemaker-api:latest` |
|
||||||
|
| Simple names | Same as upstream | `postgres:16-alpine`, `redis:7-alpine` |
|
||||||
|
| Namespaced → short | Org removed | `nocodb/nocodb` → `nocodb:0.301.3` |
|
||||||
|
| Conflict resolution | Explicit short name | `gotify/server` → `gotify`, `vaultwarden/server` → `vaultwarden` |
|
||||||
|
| Jitsi suite | `jitsi-{component}` | `jitsi-web:stable-9823`, `jitsi-prosody:stable-9823` |
|
||||||
|
| LinuxServer nginx | `ls-nginx` (avoids nginx conflict) | `ls-nginx:1.28.2` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Two Compose Files
|
||||||
|
|
||||||
|
| File | Purpose | Build? | Source mounts? | Image source |
|
||||||
|
|------|---------|--------|---------------|-------------|
|
||||||
|
| `docker-compose.yml` | Development | Yes (`build:` blocks) | Yes (`./api:/app`) | Built locally |
|
||||||
|
| `docker-compose.prod.yml` | Production | No | No | `${GITEA_REGISTRY:-gitea.bnkops.com/admin}/...` |
|
||||||
|
|
||||||
|
Release tarballs ship `docker-compose.prod.yml` renamed as `docker-compose.yml`.
|
||||||
|
|
||||||
|
The CCP template (`templates/docker-compose.yml.hbs`) generates a compose file that works like `docker-compose.prod.yml` when `USE_REGISTRY_IMAGES=true`, or like `docker-compose.yml` when `false`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ── Development ──
|
||||||
|
docker compose up -d v2-postgres redis api admin # Start dev stack
|
||||||
|
docker compose logs -f api # Watch API logs
|
||||||
|
docker compose exec api npx prisma migrate dev # Create migration
|
||||||
|
|
||||||
|
# ── Build & Publish ──
|
||||||
|
./scripts/build-and-push.sh # Build + push 4 images
|
||||||
|
./scripts/mirror-images.sh # Mirror 36 third-party images
|
||||||
|
./scripts/build-release.sh --tag v2.2.0 --upload # Package + upload release
|
||||||
|
|
||||||
|
# ── Deploy ──
|
||||||
|
curl -fsSL .../install.sh | bash # New install (release)
|
||||||
|
./scripts/upgrade.sh # Upgrade existing install
|
||||||
|
./scripts/upgrade.sh --use-registry # Fast upgrade (registry images)
|
||||||
|
|
||||||
|
# ── Verify ──
|
||||||
|
curl -s http://localhost:4000/api/health # API health check
|
||||||
|
docker compose ps # Container status
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist: Cutting a New Release
|
||||||
|
|
||||||
|
1. [ ] All code changes committed and pushed to `v2` branch
|
||||||
|
2. [ ] `docker compose up -d` works locally (smoke test)
|
||||||
|
3. [ ] `./scripts/build-and-push.sh` — builds and pushes 4 production images
|
||||||
|
4. [ ] `./scripts/mirror-images.sh` — only if third-party versions changed
|
||||||
|
5. [ ] `./scripts/build-release.sh --tag vX.Y.Z --upload` — packages and uploads tarball
|
||||||
|
6. [ ] Test clean install: `tar xzf ... && cd changemaker-lite && bash config.sh && docker compose up -d`
|
||||||
|
7. [ ] Test upgrade: `./scripts/upgrade.sh` on an existing installation
|
||||||
|
8. [ ] Verify: `curl http://localhost:4000/api/health` returns `{"status":"ok"}`
|
||||||
@ -2,17 +2,13 @@ FROM codercom/code-server:latest
|
|||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
# Install Node.js 18+ and npm
|
# Install Node.js (for npm/claude-code — code-server bundles its own node but doesn't expose it)
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
RUN apt-get update && apt-get install -y nodejs npm --no-install-recommends \
|
||||||
&& apt-get install -y nodejs
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Claude Code globally as root
|
# Install Claude Code globally
|
||||||
RUN npm install -g @anthropic-ai/claude-code
|
RUN npm install -g @anthropic-ai/claude-code
|
||||||
|
|
||||||
# Install Ollama (needs zstd for extraction)
|
|
||||||
RUN apt-get update && apt-get install -y zstd && rm -rf /var/lib/apt/lists/* \
|
|
||||||
&& curl -fsSL https://ollama.com/install.sh | sh
|
|
||||||
|
|
||||||
# Install Python and dependencies
|
# Install Python and dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@ -1,569 +0,0 @@
|
|||||||
# Social Calendar Feature Plan
|
|
||||||
|
|
||||||
**Created:** 2026-03-06
|
|
||||||
**Status:** Planning Complete — Ready for Phase A Implementation
|
|
||||||
**Branch:** v2
|
|
||||||
**Feature Flag:** `enableSocialCalendar` (new, under SiteSettings)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
A layered personal and social calendar system. Each user gets their own calendar with multiple layers (system-populated, user-created, external feeds). Calendars can be shared between users at the item, category (layer), or whole-calendar level. Shared views allow multiple users' events to appear on a merged, color-coded calendar. Admin shared views can auto-include users by role.
|
|
||||||
|
|
||||||
### Design Principles
|
|
||||||
|
|
||||||
- **Layers are the core abstraction** — every event belongs to a layer, layers control visibility and sharing
|
|
||||||
- **System layers are virtual** — shifts, tickets, polls are queried live from source tables, not duplicated
|
|
||||||
- **Recurrence uses materialization** — consistent with existing ShiftSeries pattern (generate DB rows, allow exceptions)
|
|
||||||
- **Social-first** — friend relationships gate sharing; admin views are separate and only expose system data
|
|
||||||
- **Privacy by default** — layers default to PRIVATE; users explicitly opt into sharing
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Model
|
|
||||||
|
|
||||||
### CalendarLayer
|
|
||||||
|
|
||||||
Each user has multiple layers. System layers are auto-created on first calendar access.
|
|
||||||
|
|
||||||
| Field | Type | Notes |
|
|
||||||
|-------|------|-------|
|
|
||||||
| id | String (cuid) | PK |
|
|
||||||
| userId | String | FK to User |
|
|
||||||
| name | String | "Personal", "Gym", "Google Cal", etc. |
|
|
||||||
| layerType | Enum | SYSTEM, USER, EXTERNAL |
|
|
||||||
| systemType | Enum? | SHIFTS, TICKETS, POLLS, PUBLIC_EVENTS (for SYSTEM layers only) |
|
|
||||||
| color | String | Hex color (#1890ff) |
|
|
||||||
| visibility | Enum | PRIVATE, FRIENDS, PUBLIC |
|
|
||||||
| isEnabled | Boolean | User can toggle layers on/off for themselves |
|
|
||||||
| sortOrder | Int | Display ordering |
|
|
||||||
| createdAt | DateTime | |
|
|
||||||
| updatedAt | DateTime | |
|
|
||||||
|
|
||||||
**System layers (auto-created per user):**
|
|
||||||
- My Shifts — from ShiftSignup records
|
|
||||||
- My Tickets — from EventTicket records
|
|
||||||
- My Polls — from SchedulingPollVote records
|
|
||||||
- Public Events — the existing Gancio/platform feed (togglable)
|
|
||||||
|
|
||||||
System layers are **virtual** — no CalendarItem rows are created. The API queries source tables directly and maps to the CalendarItem shape at response time.
|
|
||||||
|
|
||||||
### CalendarItem
|
|
||||||
|
|
||||||
User-created events, time blocks, and cached .ics feed entries.
|
|
||||||
|
|
||||||
| Field | Type | Notes |
|
|
||||||
|-------|------|-------|
|
|
||||||
| id | String (cuid) | PK |
|
|
||||||
| userId | String | FK to User (owner) |
|
|
||||||
| layerId | String | FK to CalendarLayer |
|
|
||||||
| title | String | |
|
|
||||||
| description | String? | Text |
|
|
||||||
| date | DateTime | Date of this occurrence |
|
|
||||||
| startTime | String | HH:MM |
|
|
||||||
| endTime | String | HH:MM |
|
|
||||||
| isAllDay | Boolean | Default false |
|
|
||||||
| itemType | Enum | EVENT, TIME_BLOCK, REMINDER |
|
|
||||||
| location | String? | |
|
|
||||||
| color | String? | Override (null = inherit layer color) |
|
|
||||||
| visibility | Enum? | PRIVATE, FRIENDS, PUBLIC (null = inherit from layer) |
|
|
||||||
| busyStatus | Enum | BUSY, TENTATIVE, FREE (default BUSY) |
|
|
||||||
| showDetailsTo | Enum | NOBODY, FRIENDS, EVERYONE (default FRIENDS) |
|
|
||||||
| recurrenceRule | Json? | See Recurrence section |
|
|
||||||
| recurrenceEnd | DateTime? | When series stops |
|
|
||||||
| seriesId | String? | Groups recurring instances |
|
|
||||||
| isException | Boolean | Edited instance that broke from pattern |
|
|
||||||
| sourceType | Enum | MANUAL, ICS_FEED |
|
|
||||||
| sourceId | String? | External reference (ics UID, etc.) |
|
|
||||||
| createdAt | DateTime | |
|
|
||||||
| updatedAt | DateTime | |
|
|
||||||
|
|
||||||
**Notes:**
|
|
||||||
- System-layer items (shifts, tickets, polls) are NOT stored as CalendarItem rows — they're virtual
|
|
||||||
- .ics feed items ARE stored as CalendarItem rows (cached from external source, read-only to user)
|
|
||||||
- MANUAL items are user-created freeform events
|
|
||||||
|
|
||||||
### CalendarFeed (.ics import)
|
|
||||||
|
|
||||||
| Field | Type | Notes |
|
|
||||||
|-------|------|-------|
|
|
||||||
| id | String (cuid) | PK |
|
|
||||||
| userId | String | FK to User |
|
|
||||||
| name | String | "Google Calendar", "Work" |
|
|
||||||
| url | String | .ics URL |
|
|
||||||
| layerId | String | FK to auto-created CalendarLayer |
|
|
||||||
| refreshInterval | Enum | FIFTEEN_MIN, HOURLY, SIX_HOUR, DAILY |
|
|
||||||
| lastFetchedAt | DateTime? | |
|
|
||||||
| lastStatus | Enum | OK, ERROR, PENDING |
|
|
||||||
| lastError | String? | Error message if failed |
|
|
||||||
| itemCount | Int | How many items imported |
|
|
||||||
| createdAt | DateTime | |
|
|
||||||
| updatedAt | DateTime | |
|
|
||||||
|
|
||||||
### SharedCalendarView
|
|
||||||
|
|
||||||
| Field | Type | Notes |
|
|
||||||
|-------|------|-------|
|
|
||||||
| id | String (cuid) | PK |
|
|
||||||
| name | String | "Weekend Crew", "All Shift Admins" |
|
|
||||||
| description | String? | |
|
|
||||||
| ownerId | String | FK to User (creator) |
|
|
||||||
| viewType | Enum | MANUAL, ROLE_BASED |
|
|
||||||
| autoIncludeRoles | Json? | ["MAP_ADMIN", "USER"] (for ROLE_BASED) |
|
|
||||||
| includedLayerTypes | Json | ["shifts", "tickets", "personal-public"] |
|
|
||||||
| shareScope | Enum | MEMBERS, PUBLIC |
|
|
||||||
| shareToken | String? | Unique token for public share URL |
|
|
||||||
| createdAt | DateTime | |
|
|
||||||
| updatedAt | DateTime | |
|
|
||||||
|
|
||||||
**ROLE_BASED views:**
|
|
||||||
- Auto-include users matching specified roles
|
|
||||||
- Only pull system layers (shifts, tickets, polls) — never personal layers
|
|
||||||
- No notifications sent to included users (admin operational tool)
|
|
||||||
- Created/managed by SUPER_ADMIN or MAP_ADMIN
|
|
||||||
|
|
||||||
**MANUAL views:**
|
|
||||||
- Members are explicitly invited via notification system
|
|
||||||
- Can include personal layers (with member consent)
|
|
||||||
- Members can decline/leave
|
|
||||||
|
|
||||||
### SharedCalendarMember
|
|
||||||
|
|
||||||
| Field | Type | Notes |
|
|
||||||
|-------|------|-------|
|
|
||||||
| id | String (cuid) | PK |
|
|
||||||
| viewId | String | FK to SharedCalendarView |
|
|
||||||
| userId | String | FK to User |
|
|
||||||
| status | Enum | INVITED, ACCEPTED, DECLINED |
|
|
||||||
| color | String | Auto-assigned from palette |
|
|
||||||
| joinedAt | DateTime? | |
|
|
||||||
| @@unique | [viewId, userId] | |
|
|
||||||
|
|
||||||
**Auto-color palette:**
|
|
||||||
```
|
|
||||||
#1890ff (blue), #52c41a (green), #fa8c16 (orange), #722ed1 (purple),
|
|
||||||
#eb2f96 (pink), #13c2c2 (cyan), #faad14 (gold), #f5222d (red),
|
|
||||||
#2f54eb (geekblue), #a0d911 (lime)
|
|
||||||
```
|
|
||||||
Assigned sequentially as members join: `PALETTE[memberIndex % length]`.
|
|
||||||
Users can override their assigned color per shared view.
|
|
||||||
|
|
||||||
### SharedViewComment
|
|
||||||
|
|
||||||
| Field | Type | Notes |
|
|
||||||
|-------|------|-------|
|
|
||||||
| id | String (cuid) | PK |
|
|
||||||
| viewId | String | FK to SharedCalendarView |
|
|
||||||
| userId | String | FK to User |
|
|
||||||
| itemDate | String | YYYY-MM-DD (which date this comment is about) |
|
|
||||||
| itemId | String? | Optional: specific CalendarItem or source item ID |
|
|
||||||
| content | String | Text |
|
|
||||||
| createdAt | DateTime | |
|
|
||||||
|
|
||||||
### SharedViewReaction
|
|
||||||
|
|
||||||
| Field | Type | Notes |
|
|
||||||
|-------|------|-------|
|
|
||||||
| id | String (cuid) | PK |
|
|
||||||
| viewId | String | FK to SharedCalendarView |
|
|
||||||
| userId | String | FK to User |
|
|
||||||
| itemId | String | CalendarItem or source item ID (e.g., "shift-abc123") |
|
|
||||||
| emoji | String | Single emoji or shortcode |
|
|
||||||
| createdAt | DateTime | |
|
|
||||||
| @@unique | [viewId, userId, itemId, emoji] | One reaction type per user per item |
|
|
||||||
|
|
||||||
### CalendarExportToken (.ics export)
|
|
||||||
|
|
||||||
| Field | Type | Notes |
|
|
||||||
|-------|------|-------|
|
|
||||||
| id | String (cuid) | PK |
|
|
||||||
| userId | String | FK to User |
|
|
||||||
| token | String | Unique, random (for URL auth) |
|
|
||||||
| includePersonal | Boolean | Whether personal events are exported |
|
|
||||||
| includeLayers | Json? | Array of layer IDs (null = all enabled) |
|
|
||||||
| createdAt | DateTime | |
|
|
||||||
|
|
||||||
Export URL: `GET /api/calendar/feed/:userId/:token.ics`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recurrence Model
|
|
||||||
|
|
||||||
Uses **materialization** (consistent with existing ShiftSeries pattern):
|
|
||||||
|
|
||||||
1. User creates a recurring event with a recurrence rule
|
|
||||||
2. System generates CalendarItem rows for the next 3 months
|
|
||||||
3. Background job (BullMQ, daily) extends series forward by 1 month
|
|
||||||
4. Individual instances can be edited (becomes `isException: true`) or deleted
|
|
||||||
5. Editing the series template updates all non-exception future instances
|
|
||||||
|
|
||||||
### Recurrence Rule JSON
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"frequency": "DAILY | WEEKLY | BIWEEKLY | MONTHLY",
|
|
||||||
"daysOfWeek": [1, 3, 5],
|
|
||||||
"dayOfMonth": 15,
|
|
||||||
"interval": 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `WEEKLY` + `daysOfWeek: [1,3,5]` = every Mon/Wed/Fri
|
|
||||||
- `MONTHLY` + `dayOfMonth: 15` = 15th of every month
|
|
||||||
- `BIWEEKLY` + `daysOfWeek: [2,4]` = every other Tue/Thu
|
|
||||||
- `interval` for skip patterns (every 2 weeks, every 3 months)
|
|
||||||
|
|
||||||
### Recurrence Edit Options (UI)
|
|
||||||
|
|
||||||
When editing a recurring event instance:
|
|
||||||
- "This event only" — marks as exception, edits the single instance
|
|
||||||
- "This and future events" — updates template + regenerates future non-exception instances
|
|
||||||
- "All events in series" — updates template + all instances (including past, excluding exceptions)
|
|
||||||
|
|
||||||
When deleting:
|
|
||||||
- "This event only" — soft-delete the single instance
|
|
||||||
- "This and future events" — delete future instances, set recurrenceEnd on template
|
|
||||||
- "All events" — delete entire series
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Time Block Visibility (Configurable per item)
|
|
||||||
|
|
||||||
| `showDetailsTo` | Friends see | Public sees |
|
|
||||||
|-----------------|-------------|-------------|
|
|
||||||
| NOBODY | "Busy 2-4pm" | "Busy 2-4pm" |
|
|
||||||
| FRIENDS | "Dentist 2-4pm" | "Busy 2-4pm" |
|
|
||||||
| EVERYONE | "Dentist 2-4pm" | "Dentist 2-4pm" |
|
|
||||||
|
|
||||||
Combined with `busyStatus`:
|
|
||||||
- **BUSY** — solid color block
|
|
||||||
- **TENTATIVE** — dashed/lighter block
|
|
||||||
- **FREE** — no block shown (informational only, e.g., "Available for meetings")
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notification Types (reusing existing system)
|
|
||||||
|
|
||||||
| Type | Message | Trigger |
|
|
||||||
|------|---------|---------|
|
|
||||||
| SHARED_VIEW_INVITE | "Alice invited you to 'Weekend Crew' calendar" | Manual shared view invite |
|
|
||||||
| SHARED_VIEW_ACCEPTED | "Bob accepted your invite to 'Weekend Crew'" | Member accepts |
|
|
||||||
| CALENDAR_EVENT_INVITE | "Alice added you to 'Planning Meeting' on Mar 10" | Phase B: event-level sharing |
|
|
||||||
| CALENDAR_REMINDER | "Reminder: Team standup in 15 minutes" | Future: optional reminders |
|
|
||||||
|
|
||||||
Role-based admin views do NOT trigger notifications (admin operational tool using only system data).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Availability Finder (Phase B)
|
|
||||||
|
|
||||||
A dedicated mode within shared calendar views:
|
|
||||||
|
|
||||||
1. Toggle "Find Available Time" on a shared view
|
|
||||||
2. System overlays all members' BUSY/TENTATIVE time blocks
|
|
||||||
3. Highlights gaps where ALL members are free
|
|
||||||
4. Optional: filter by time range ("only show weekday 9am-5pm slots")
|
|
||||||
5. Click a free slot to create an event and auto-invite all members
|
|
||||||
|
|
||||||
Visual: green highlight on free slots, red/orange on conflicts, member avatars on busy blocks.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API Routes
|
|
||||||
|
|
||||||
### Phase A (Personal Calendar)
|
|
||||||
|
|
||||||
```
|
|
||||||
# Layers
|
|
||||||
GET /api/calendar/layers — list user's layers
|
|
||||||
POST /api/calendar/layers — create custom layer
|
|
||||||
PATCH /api/calendar/layers/:id — update layer (name, color, visibility, enabled)
|
|
||||||
DELETE /api/calendar/layers/:id — delete custom layer (+ its items)
|
|
||||||
|
|
||||||
# Calendar Items
|
|
||||||
GET /api/calendar/items — list items in date range (all enabled layers merged)
|
|
||||||
POST /api/calendar/items — create item (event, time block, reminder)
|
|
||||||
PATCH /api/calendar/items/:id — update item
|
|
||||||
DELETE /api/calendar/items/:id — delete item
|
|
||||||
|
|
||||||
# Recurrence
|
|
||||||
POST /api/calendar/items/:id/series — edit series (this-only, this-and-future, all)
|
|
||||||
DELETE /api/calendar/items/:id/series — delete series (this-only, this-and-future, all)
|
|
||||||
|
|
||||||
# Unified personal view (merges system layers + user items)
|
|
||||||
GET /api/calendar/my — personal calendar (date range, layer filters)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase B (Sharing + Social)
|
|
||||||
|
|
||||||
```
|
|
||||||
# Shared Views
|
|
||||||
GET /api/calendar/shared — list shared views I own or am a member of
|
|
||||||
POST /api/calendar/shared — create shared view
|
|
||||||
PATCH /api/calendar/shared/:id — update shared view
|
|
||||||
DELETE /api/calendar/shared/:id — delete shared view (owner only)
|
|
||||||
|
|
||||||
# Members
|
|
||||||
POST /api/calendar/shared/:id/invite — invite user(s) to shared view
|
|
||||||
PATCH /api/calendar/shared/:id/respond — accept/decline invite
|
|
||||||
DELETE /api/calendar/shared/:id/leave — leave a shared view
|
|
||||||
GET /api/calendar/shared/:id/members — list members + colors
|
|
||||||
|
|
||||||
# Merged calendar data
|
|
||||||
GET /api/calendar/shared/:id/items — merged items from all members
|
|
||||||
|
|
||||||
# Event-level sharing
|
|
||||||
POST /api/calendar/items/:id/share — share specific item with friend(s)
|
|
||||||
|
|
||||||
# Comments & Reactions (on shared views)
|
|
||||||
GET /api/calendar/shared/:id/comments?date=YYYY-MM-DD
|
|
||||||
POST /api/calendar/shared/:id/comments
|
|
||||||
DELETE /api/calendar/shared/:id/comments/:commentId
|
|
||||||
POST /api/calendar/shared/:id/reactions
|
|
||||||
DELETE /api/calendar/shared/:id/reactions/:reactionId
|
|
||||||
|
|
||||||
# Availability finder
|
|
||||||
GET /api/calendar/shared/:id/availability?start=&end=&dayStart=09:00&dayEnd=17:00
|
|
||||||
|
|
||||||
# Friend's public calendar
|
|
||||||
GET /api/calendar/user/:userId — view a friend's public items
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase C (.ics Integration)
|
|
||||||
|
|
||||||
```
|
|
||||||
# Feeds (import)
|
|
||||||
GET /api/calendar/feeds — list user's subscribed feeds
|
|
||||||
POST /api/calendar/feeds — subscribe to .ics URL
|
|
||||||
PATCH /api/calendar/feeds/:id — update feed settings
|
|
||||||
DELETE /api/calendar/feeds/:id — unsubscribe (deletes layer + cached items)
|
|
||||||
POST /api/calendar/feeds/:id/refresh — force refresh now
|
|
||||||
|
|
||||||
# Export
|
|
||||||
GET /api/calendar/export/token — get or create export token
|
|
||||||
DELETE /api/calendar/export/token — revoke export token
|
|
||||||
GET /api/calendar/feed/:userId/:token.ics — public .ics feed (no auth, token in URL)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase D (Admin Shared Views)
|
|
||||||
|
|
||||||
```
|
|
||||||
# Admin role-based views (requireRole: SUPER_ADMIN, MAP_ADMIN)
|
|
||||||
POST /api/admin/calendar/shared — create role-based shared view
|
|
||||||
PATCH /api/admin/calendar/shared/:id — update
|
|
||||||
DELETE /api/admin/calendar/shared/:id — delete
|
|
||||||
GET /api/admin/calendar/shared/:id/items — merged system-layer data for matching users
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Frontend Pages & Components
|
|
||||||
|
|
||||||
### Phase A
|
|
||||||
|
|
||||||
| Component | Location | Description |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| MyCalendarPage | `volunteer/MyCalendarPage.tsx` | Personal calendar (main view) |
|
|
||||||
| CalendarLayerPanel | `components/calendar/CalendarLayerPanel.tsx` | Sidebar: layer list with toggles, colors, visibility |
|
|
||||||
| CalendarItemModal | `components/calendar/CalendarItemModal.tsx` | Create/edit event, time block, or reminder |
|
|
||||||
| RecurrenceEditor | `components/calendar/RecurrenceEditor.tsx` | Recurrence rule builder (frequency, days, end date) |
|
|
||||||
| PersonalCalendarView | `components/calendar/PersonalCalendarView.tsx` | Month/week/day calendar with layer color-coding |
|
|
||||||
| MobileDayView | `components/calendar/MobileDayView.tsx` | Day/3-day swipeable view for mobile |
|
|
||||||
|
|
||||||
**Mobile UX:** Day or 3-day swipeable view (not full month grid). Swipe left/right to navigate days. Layer toggles in a collapsible bottom sheet.
|
|
||||||
|
|
||||||
### Phase B
|
|
||||||
|
|
||||||
| Component | Location | Description |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| SharedCalendarsPage | `volunteer/SharedCalendarsPage.tsx` | List of shared views I'm in |
|
|
||||||
| SharedCalendarView | `components/calendar/SharedCalendarView.tsx` | Merged multi-user calendar with member colors |
|
|
||||||
| SharedViewMembersPanel | `components/calendar/SharedViewMembersPanel.tsx` | Member list, color overrides, invite button |
|
|
||||||
| AvailabilityFinder | `components/calendar/AvailabilityFinder.tsx` | Free/busy overlay with slot highlighting |
|
|
||||||
| CalendarComments | `components/calendar/CalendarComments.tsx` | Comment thread for a date in shared view |
|
|
||||||
| CalendarReactions | `components/calendar/CalendarReactions.tsx` | Emoji reactions on items |
|
|
||||||
| FriendCalendarPage | `volunteer/FriendCalendarPage.tsx` | View a friend's public calendar |
|
|
||||||
|
|
||||||
### Phase C
|
|
||||||
|
|
||||||
| Component | Location | Description |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| CalendarFeedsPanel | `components/calendar/CalendarFeedsPanel.tsx` | Manage .ics subscriptions |
|
|
||||||
| CalendarExportPanel | `components/calendar/CalendarExportPanel.tsx` | Export token management, copy URL |
|
|
||||||
|
|
||||||
### Phase D
|
|
||||||
|
|
||||||
| Component | Location | Description |
|
|
||||||
|-----------|----------|-------------|
|
|
||||||
| AdminSharedViewsPage | `pages/AdminSharedViewsPage.tsx` | Admin: create/manage role-based views |
|
|
||||||
| AdminCalendarOverview | `components/calendar/AdminCalendarOverview.tsx` | Big shift/event overview for admins |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Navigation & Routing
|
|
||||||
|
|
||||||
### Volunteer Portal
|
|
||||||
- Footer nav: add "Calendar" tab (CalendarOutlined icon)
|
|
||||||
- `/volunteer/calendar` — MyCalendarPage
|
|
||||||
- `/volunteer/calendar/shared` — SharedCalendarsPage
|
|
||||||
- `/volunteer/calendar/shared/:id` — SharedCalendarView
|
|
||||||
- `/volunteer/calendar/friend/:userId` — FriendCalendarPage
|
|
||||||
|
|
||||||
### Admin
|
|
||||||
- Sidebar under existing section: "Calendar Overview"
|
|
||||||
- `/app/calendar/shared` — AdminSharedViewsPage
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase Breakdown
|
|
||||||
|
|
||||||
### Phase A: Personal Calendar + Layers + Freeform Events
|
|
||||||
**Scope:**
|
|
||||||
- [x] Prisma models: CalendarLayer, CalendarItem, CalendarFeed, SharedCalendarView, SharedCalendarMember, SharedViewComment, SharedViewReaction, CalendarExportToken (+ 12 enums)
|
|
||||||
- [x] Auto-create system layers on first calendar access (ensureSystemLayers)
|
|
||||||
- [x] CalendarItem CRUD (create, read, update, delete)
|
|
||||||
- [x] Recurrence: create series (materialize 3 months), edit/delete with scope options (THIS_ONLY/THIS_AND_FUTURE/ALL)
|
|
||||||
- [ ] BullMQ job: extend recurring series daily (add 1 month of future instances)
|
|
||||||
- [x] Personal calendar API: GET /api/calendar/my (merge system layers + user items)
|
|
||||||
- [x] System layer queries: shifts (from ShiftSignup), tickets (from Ticket), polls (from SchedulingPollVote)
|
|
||||||
- [x] Layer CRUD: create custom layers, toggle on/off, set color
|
|
||||||
- [x] Layer visibility settings (PRIVATE/FRIENDS/PUBLIC) — stored but not enforced until Phase B
|
|
||||||
- [x] MyCalendarPage: month view (desktop), day/3-day view (mobile)
|
|
||||||
- [x] CalendarLayerPanel: sidebar with layer toggles, color pickers, inline editing, grouped by type
|
|
||||||
- [x] CalendarItemModal: create/edit form with item type, recurrence, time block settings, scope selector
|
|
||||||
- [x] RecurrenceEditor: frequency/days/interval/end-date with preview text
|
|
||||||
- [x] PersonalCalendarView: desktop month view with layer-colored items
|
|
||||||
- [x] MobileDayView: day view with time grid, current time indicator, floating add button
|
|
||||||
- [x] Volunteer footer nav: "Calendar" tab (gated behind enableSocialCalendar)
|
|
||||||
- [x] Feature flag: enableSocialCalendar in SiteSettings, Zod schema, frontend types, FeatureGate
|
|
||||||
- [x] Settings page toggle added ("Social Calendar" in People & Engagement section)
|
|
||||||
|
|
||||||
### Phase B: Sharing + Social
|
|
||||||
**Scope:**
|
|
||||||
- [ ] Prisma models: SharedCalendarView, SharedCalendarMember, SharedViewComment, SharedViewReaction
|
|
||||||
- [ ] SharedCalendarView CRUD
|
|
||||||
- [ ] Invite flow: send invite via notification system, accept/decline/leave
|
|
||||||
- [ ] Merged calendar API: query all members' items with layer type filtering
|
|
||||||
- [ ] Auto-color assignment for members
|
|
||||||
- [ ] Layer visibility enforcement (PRIVATE/FRIENDS/PUBLIC filtering based on relationship)
|
|
||||||
- [ ] Event-level sharing: share a specific item with friend(s) via notification
|
|
||||||
- [ ] Comments on shared view dates/items
|
|
||||||
- [ ] Emoji reactions on shared view items
|
|
||||||
- [ ] Availability finder: free/busy overlay, slot highlighting, time range filter
|
|
||||||
- [ ] Friend's public calendar view
|
|
||||||
- [ ] SharedCalendarsPage, SharedCalendarView components
|
|
||||||
- [ ] AvailabilityFinder component
|
|
||||||
- [ ] CalendarComments, CalendarReactions components
|
|
||||||
- [ ] Public share URL (shareToken for unauthenticated view)
|
|
||||||
|
|
||||||
### Phase C: .ics Integration
|
|
||||||
**Scope:**
|
|
||||||
- [x] Prisma models: CalendarFeed, CalendarExportToken (already existed from Phase A migration)
|
|
||||||
- [x] .ics feed parser (node-ical v0.25.5)
|
|
||||||
- [x] BullMQ job: refresh feeds every 15 minutes (calendar-feed-refresh queue)
|
|
||||||
- [x] Feed CRUD: subscribe, update, delete, force refresh
|
|
||||||
- [x] Auto-create EXTERNAL layer per feed, cache items as CalendarItem rows (sourceType: ICS_FEED)
|
|
||||||
- [x] .ics export: generate feed from user's calendar via ical-generator v10, token-authenticated URL
|
|
||||||
- [x] Export token management (create, list, revoke)
|
|
||||||
- [x] CalendarFeedsPanel, CalendarExportPanel components
|
|
||||||
- [x] MyCalendarPage settings Drawer integration (gear icon)
|
|
||||||
|
|
||||||
### Phase D: Admin Shared Views
|
|
||||||
**Scope:**
|
|
||||||
- [ ] Role-based SharedCalendarView (viewType: ROLE_BASED)
|
|
||||||
- [ ] Auto-include users by role(s) — query live, no member rows needed
|
|
||||||
- [ ] Only expose system layers (shifts, tickets, polls) — no personal data
|
|
||||||
- [ ] No notifications to included users
|
|
||||||
- [ ] Admin routes (requireRole: SUPER_ADMIN, MAP_ADMIN)
|
|
||||||
- [ ] AdminSharedViewsPage
|
|
||||||
- [ ] AdminCalendarOverview (big shift/event dashboard)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
### Extending UnifiedCalendar
|
|
||||||
|
|
||||||
The existing `UnifiedCalendar` component and `unified-calendar.service.ts` remain as the **public** calendar. The new personal calendar service (`calendar.service.ts`) reuses the same source queries (shifts, Gancio, polls, ticketed events) but filters to the user's own records and merges with their CalendarItem rows.
|
|
||||||
|
|
||||||
### Recurrence Background Job
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// jobs/calendar-recurrence.job.ts
|
|
||||||
// Runs daily via BullMQ repeatable job
|
|
||||||
// 1. Find all CalendarItems with recurrenceRule where latest materialized date < now + 3 months
|
|
||||||
// 2. Generate new instances up to 3 months ahead
|
|
||||||
// 3. Skip dates that already have an instance (idempotent)
|
|
||||||
```
|
|
||||||
|
|
||||||
### .ics Feed Refresh Job
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// jobs/calendar-feed-refresh.job.ts
|
|
||||||
// Runs every 15 minutes via BullMQ repeatable job
|
|
||||||
// 1. Find feeds where lastFetchedAt + refreshInterval < now
|
|
||||||
// 2. Fetch .ics URL, parse events
|
|
||||||
// 3. Upsert CalendarItem rows (match on sourceId = ics UID)
|
|
||||||
// 4. Delete items no longer in feed
|
|
||||||
// 5. Update feed status
|
|
||||||
```
|
|
||||||
|
|
||||||
### Privacy Boundaries
|
|
||||||
|
|
||||||
| Scenario | What's visible |
|
|
||||||
|----------|---------------|
|
|
||||||
| Viewing own calendar | Everything (all layers, all items) |
|
|
||||||
| Friend views your calendar | Items on FRIENDS or PUBLIC visibility layers, plus items with individual FRIENDS/PUBLIC override |
|
|
||||||
| Public profile calendar | Only PUBLIC visibility layers and PUBLIC override items |
|
|
||||||
| Admin role-based view | Only system layers (shifts, tickets, polls) for users matching role filter |
|
|
||||||
| Shared view (MANUAL) | Items from includedLayerTypes on layers with appropriate visibility for the viewer |
|
|
||||||
| Time blocks (BUSY) | Title shown per showDetailsTo setting, always shows busy bar |
|
|
||||||
|
|
||||||
### Performance Considerations
|
|
||||||
|
|
||||||
- CalendarItem table will grow with materialized recurrence — add indexes on (userId, date), (layerId, date), (seriesId)
|
|
||||||
- System layers query source tables directly — leverage existing indexes on ShiftSignup, EventTicket, etc.
|
|
||||||
- .ics feed items are cached — only re-parsed on refresh interval
|
|
||||||
- Shared view queries can be expensive (N members x M layers) — cache merged results in Redis (2min TTL, bust on member change)
|
|
||||||
- Availability finder operates on time blocks only — narrow query scope
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tracking Log
|
|
||||||
|
|
||||||
### 2026-03-06 — Planning Complete
|
|
||||||
- Brainstormed feature across 3 rounds of refinement
|
|
||||||
- Decided on layer-based architecture (system, user, external layers)
|
|
||||||
- Recurrence uses materialization (consistent with ShiftSeries pattern)
|
|
||||||
- Time block visibility is configurable per item (showDetailsTo: NOBODY/FRIENDS/EVERYONE)
|
|
||||||
- Shared views support manual (invite-based) and role-based (admin, system data only)
|
|
||||||
- Availability finder included in Phase B
|
|
||||||
- Comments and reactions on shared view items included in Phase B
|
|
||||||
- .ics import and export in Phase C
|
|
||||||
- Admin role-based views in Phase D (no personal data, no notifications)
|
|
||||||
- Reuse existing notification system for invites
|
|
||||||
- Auto-color assignment for shared view members with user override option
|
|
||||||
- Mobile UX: day/3-day swipeable view instead of month grid
|
|
||||||
|
|
||||||
### 2026-03-06 — Phase A Implementation Complete
|
|
||||||
- Schema: 8 models, 12 enums, migration `20260306203326_social_calendar_layers_items` applied
|
|
||||||
- Fixed pre-existing migration ordering issue (ticketed_events create must come before alter)
|
|
||||||
- Backend: calendar.service.ts (layer mgmt, item CRUD, recurrence materialization, personal calendar merge), calendar.routes.ts (9 endpoints), calendar.schemas.ts (Zod validation)
|
|
||||||
- Frontend: 5 new components (CalendarLayerPanel, CalendarItemModal, RecurrenceEditor, PersonalCalendarView, MobileDayView), MyCalendarPage
|
|
||||||
- Navigation: VolunteerFooterNav Calendar tab, App.tsx route, SettingsPage toggle
|
|
||||||
- Smoke tested: layers auto-create, item CRUD works, recurring events materialize correctly (Weekly Mon/Wed/Fri generated 11 instances through June)
|
|
||||||
- Both API and Admin compile with zero TypeScript errors
|
|
||||||
- Remaining Phase A item: BullMQ job for extending recurring series (not critical for launch, series materializes 3 months on creation)
|
|
||||||
|
|
||||||
### 2026-03-07 — Phase C Implementation Complete
|
|
||||||
- Backend: feed.schemas.ts (3 Zod schemas), feed.service.ts (feed CRUD, ICS parsing, RRULE materialization, export generation), feed.routes.ts (1 public + 8 auth routes), calendar-feed-queue.service.ts (BullMQ 15min repeatable job)
|
|
||||||
- Dependencies: node-ical v0.25.5 (ICS parsing), ical-generator v10.0.0 (ICS output)
|
|
||||||
- Feed import: streaming body read with 5MB limit, 1000 event cap, RRULE materialization via rrule.between(), stale event cleanup, status tracking (OK/ERROR/PENDING)
|
|
||||||
- Feed export: 32-byte random token, configurable layer/personal inclusion, past 1 month + future 3 months, standard iCalendar output with Content-Type: text/calendar
|
|
||||||
- Frontend: CalendarFeedsPanel (add/edit/delete/refresh with status badges), CalendarExportPanel (create/copy/revoke tokens), settings Drawer in MyCalendarPage (gear icon)
|
|
||||||
- Types: CalendarFeed, CalendarExportToken, CalendarFeedStatus, CalendarFeedInterval added to admin/src/types/api.ts
|
|
||||||
- server.ts: feedRoutes mounted before calendarRoutes (public .ics route needs no auth), queue worker started on bootstrap, graceful shutdown
|
|
||||||
- Smoke tested: Google US Holidays feed → 317 events imported with status OK; export token → valid .ics with VEVENT entries; revoke → 404
|
|
||||||
- Docker gotcha: anonymous volume `/app/node_modules` caches old dependencies — must `docker compose rm -sf api` to clear when adding new npm packages
|
|
||||||
- Both API and Admin compile with zero TypeScript errors
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
# Social Connections System — Implementation Plan
|
|
||||||
|
|
||||||
See the full plan in the conversation transcript. This file tracks implementation progress.
|
|
||||||
|
|
||||||
## Phase Status
|
|
||||||
|
|
||||||
| Phase | Description | Status |
|
|
||||||
|-------|-------------|--------|
|
|
||||||
| 1 | Feature Flag + Social Module Skeleton | COMPLETE |
|
|
||||||
| 2 | Friendship API (Send, Accept, Decline, Cancel, Unfriend) | COMPLETE |
|
|
||||||
| 3 | Block/Unblock API + Privacy Settings API | COMPLETE |
|
|
||||||
| 4 | User Social Profile + Volunteer Portal UI Foundation | COMPLETE |
|
|
||||||
| 5 | In-App Notification System (Bell Icon + Dropdown) | COMPLETE |
|
|
||||||
| 6 | Social Activity Feed (Friends' Activities) | COMPLETE |
|
|
||||||
| 7 | CRM Bridge (Auto-Connect + Friend Suggestions) | COMPLETE |
|
|
||||||
| 8 | Poke System + Video Recommendations | COMPLETE |
|
|
||||||
| 9 | Close Friends + Friends Management Page | COMPLETE (merged into Phase 4 UI) |
|
|
||||||
| 10 | Email Digest Notifications | COMPLETE |
|
|
||||||
| 11 | Social Integration with Existing Features | COMPLETE |
|
|
||||||
| 12 | Rocket.Chat DM Integration | COMPLETE |
|
|
||||||
| 13 | Group/Team Features (Shift Teams, Campaign Teams) | COMPLETE |
|
|
||||||
| 14 | Gamification (Achievements, Streaks, Leaderboards) | COMPLETE |
|
|
||||||
| 15 | Real-Time Features (SSE for Live Notifications, Online Status) | COMPLETE |
|
|
||||||
|
|
||||||
## Files Created/Modified
|
|
||||||
|
|
||||||
### Backend (API)
|
|
||||||
- `api/prisma/schema.prisma` — added `enableSocial` to SiteSettings
|
|
||||||
- `api/prisma/migrations/20260224215259_add_enable_social/` — migration
|
|
||||||
- `api/src/modules/social/` — new module directory
|
|
||||||
- `social.routes.ts` — main router mounting sub-routers
|
|
||||||
- `social.schemas.ts` — Zod schemas (friendship, privacy, notification)
|
|
||||||
- `social.middleware.ts` — `checkSocialEnabled` feature gate
|
|
||||||
- `social.rate-limits.ts` — rate limiters (friend request, social action)
|
|
||||||
- `friendship.service.ts` — full friendship CRUD + notifications
|
|
||||||
- `friendship.routes.ts` — 10 friendship endpoints
|
|
||||||
- `block.service.ts` — block/unblock with auto-unfriend
|
|
||||||
- `block.routes.ts` — 3 block endpoints
|
|
||||||
- `privacy.service.ts` — privacy settings get/update (auto-create defaults)
|
|
||||||
- `privacy.routes.ts` — 2 privacy endpoints
|
|
||||||
- `notification.service.ts` — notification CRUD + preferences (respects opt-outs)
|
|
||||||
- `notification.routes.ts` — 7 notification endpoints
|
|
||||||
- `profile.routes.ts` — user profile view (own + other, privacy-filtered)
|
|
||||||
- `api/src/modules/settings/settings.schemas.ts` — added `enableSocial`
|
|
||||||
- `api/src/server.ts` — mounted socialRouter at `/api/social`
|
|
||||||
|
|
||||||
### Frontend (Admin)
|
|
||||||
- `admin/src/types/social.ts` — TypeScript interfaces
|
|
||||||
- `admin/src/stores/social.store.ts` — Zustand social store
|
|
||||||
- `admin/src/components/social/` — new directory
|
|
||||||
- `UserAvatar.tsx` — initials avatar with userId-based color
|
|
||||||
- `FriendButton.tsx` — context-aware friend action button
|
|
||||||
- `NotificationBell.tsx` — bell icon + dropdown (30s polling)
|
|
||||||
- `admin/src/pages/volunteer/` — new pages
|
|
||||||
- `SocialProfilePage.tsx` — own + other user profile
|
|
||||||
- `FriendsPage.tsx` — friends management (tabs: friends, requests, sent, blocked)
|
|
||||||
- `NotificationsPage.tsx` — full notification list + preferences
|
|
||||||
- `admin/src/components/VolunteerLayout.tsx` — added NotificationBell
|
|
||||||
- `admin/src/components/VolunteerFooterNav.tsx` — added "Friends" nav item
|
|
||||||
- `admin/src/components/FeatureGate.tsx` — added `enableSocial`
|
|
||||||
- `admin/src/types/api.ts` — added `enableSocial` to SiteSettings
|
|
||||||
- `admin/src/pages/SettingsPage.tsx` — added toggle
|
|
||||||
- `admin/src/App.tsx` — added 6 new volunteer routes
|
|
||||||
|
|
||||||
### Phase 6 — Social Activity Feed
|
|
||||||
- `api/src/modules/social/feed.service.ts` — aggregates 4 activity types + Redis cache (2-min TTL)
|
|
||||||
- `api/src/modules/social/feed.routes.ts` — GET `/` (friend feed), GET `/my` (own activity)
|
|
||||||
- `admin/src/components/social/FeedCard.tsx` — activity card with type-based icon/color
|
|
||||||
- `admin/src/pages/volunteer/SocialFeedPage.tsx` — feed page with suggestions widget
|
|
||||||
|
|
||||||
### Phase 7 — CRM Bridge + Suggestions
|
|
||||||
- `api/src/modules/social/suggestions.service.ts` — ranked suggestions (household/mutual/shifts/campaigns)
|
|
||||||
- `api/src/modules/social/suggestions.routes.ts` — GET `/`, POST `/:userId/dismiss`
|
|
||||||
- `admin/src/components/social/FriendSuggestions.tsx` — horizontal scroll suggestions widget
|
|
||||||
- `admin/src/pages/volunteer/DiscoverPage.tsx` — search + suggestions page
|
|
||||||
|
|
||||||
### Phase 8 — Poke System + Video Recommendations
|
|
||||||
- `api/src/modules/social/poke.service.ts` — poke CRUD + 24h Redis cooldown per pair
|
|
||||||
- `api/src/modules/social/poke.routes.ts` — POST `/`, GET `/`, GET `/count`, POST `/:id/read`, GET `/cooldown/:userId`
|
|
||||||
- `api/src/modules/social/recommendation.service.ts` — video recommendation CRUD + duplicate detection
|
|
||||||
- `api/src/modules/social/recommendation.routes.ts` — POST `/`, GET `/`, GET `/sent`, GET `/count`, POST `/:id/read`
|
|
||||||
- `admin/src/components/social/PokeButton.tsx` — poke button with cooldown indicator
|
|
||||||
- `admin/src/components/social/RecommendVideoModal.tsx` — friend + video picker modal
|
|
||||||
- `admin/src/pages/volunteer/SocialProfilePage.tsx` — added PokeButton
|
|
||||||
|
|
||||||
### Phase 10 — Email Digest Notifications
|
|
||||||
- `api/prisma/migrations/20260224232546_add_digest_frequency/` — adds digestFrequency + lastDigestSentAt
|
|
||||||
- `api/src/services/social-digest.service.ts` — daily scan, generates digest emails
|
|
||||||
- `api/src/templates/email/social-digest.html` + `.txt` — digest email templates
|
|
||||||
- `api/src/server.ts` — added daily social digest scan interval
|
|
||||||
- `admin/src/pages/volunteer/NotificationsPage.tsx` — added digest frequency selector
|
|
||||||
|
|
||||||
### Phase 11 — Social Integration with Existing Features
|
|
||||||
- `api/src/modules/social/integration.service.ts` — friends on shifts, campaigns, and active map sessions (privacy-filtered)
|
|
||||||
- `api/src/modules/social/integration.routes.ts` — 3 endpoints: shifts/:id/friends, campaigns/:id/friends, map/friends
|
|
||||||
- `admin/src/components/social/FriendsAttendingBadge.tsx` — "N friends attending" badge with stacked avatars
|
|
||||||
- `admin/src/components/social/FriendsCampaignBadge.tsx` — "N friends participated" badge with stacked avatars
|
|
||||||
- `admin/src/components/social/FriendsOnMap.tsx` — floating panel showing friends currently canvassing (60s poll)
|
|
||||||
- `admin/src/pages/public/ShiftsPage.tsx` — added FriendsAttendingBadge per shift card
|
|
||||||
- `admin/src/pages/public/CampaignPage.tsx` — added FriendsCampaignBadge in hero section
|
|
||||||
- `admin/src/pages/volunteer/VolunteerShiftsPage.tsx` — added FriendsAttendingBadge per shift card
|
|
||||||
- `admin/src/pages/volunteer/VolunteerMapPage.tsx` — added FriendsOnMap overlay
|
|
||||||
- `admin/src/types/social.ts` — added FriendOnShift, FriendOnCampaign, FriendOnMap types
|
|
||||||
|
|
||||||
### Phase 12 — Rocket.Chat DM Integration
|
|
||||||
- `api/src/modules/social/messaging.service.ts` — openDM: provisions both users, creates DM room, returns roomId
|
|
||||||
- `api/src/modules/social/profile.routes.ts` — added POST `/:userId/dm` endpoint
|
|
||||||
- `api/src/services/rocketchat.client.ts` — added `createDM(usernames)` method
|
|
||||||
- `admin/src/components/social/MessageButton.tsx` — DM button (opens chat widget, RC-gated)
|
|
||||||
- `admin/src/pages/volunteer/SocialProfilePage.tsx` — added MessageButton for accepted friends
|
|
||||||
|
|
||||||
### Phase 13 — Group/Team Features
|
|
||||||
- `api/prisma/schema.prisma` — added SocialGroup, SocialGroupMember models + SocialGroupType enum + User.socialGroupMemberships relation
|
|
||||||
- `api/prisma/migrations/20260225000017_add_social_groups/` — migration creating social_groups + social_group_members tables
|
|
||||||
- `api/src/modules/social/group.service.ts` — getOrCreate, syncShiftTeam, syncCampaignTeam, listMyGroups, getGroupDetail
|
|
||||||
- `api/src/modules/social/group.routes.ts` — GET `/` (my groups), GET `/:id` (group detail)
|
|
||||||
- `api/src/modules/social/social.routes.ts` — mounted groupRouter at `/groups`
|
|
||||||
- `api/src/modules/map/shifts/shifts.service.ts` — added fire-and-forget groupService.syncShiftTeam() on all signup/cancel events
|
|
||||||
- `api/src/modules/influence/campaign-emails/campaign-emails.service.ts` — added fire-and-forget groupService.syncCampaignTeam() on email creation
|
|
||||||
- `admin/src/types/social.ts` — added SocialGroupSummary, SocialGroupDetail interfaces
|
|
||||||
- `admin/src/components/social/GroupCard.tsx` — group card with type-based icon/color
|
|
||||||
- `admin/src/pages/volunteer/GroupDetailPage.tsx` — group detail with member list + FriendButton per member
|
|
||||||
- `admin/src/pages/volunteer/FriendsPage.tsx` — added "Groups" tab
|
|
||||||
- `admin/src/App.tsx` — added `/volunteer/groups/:id` route
|
|
||||||
|
|
||||||
### Phase 14 — Gamification (Achievements, Streaks, Leaderboards)
|
|
||||||
- `api/src/modules/social/achievements.service.ts` — 11 achievements (4 shift, 4 canvass, 2 campaign, 2 social), checkAndUnlock, getLeaderboard (raw SQL), getVolunteerStats (on-the-fly computed)
|
|
||||||
- `api/src/modules/social/achievements.routes.ts` — 6 endpoints: achievements, definitions, stats, stats/:userId, user/:userId, leaderboard
|
|
||||||
- `api/src/modules/social/social.routes.ts` — mounted achievementsRouter at `/achievements`
|
|
||||||
- `api/src/modules/map/canvass/canvass.service.ts` — added achievements.checkAndUnlock after recordVisit
|
|
||||||
- `api/src/modules/map/shifts/shifts.service.ts` — added achievements.checkAndUnlock after signup events (admin, public, volunteer)
|
|
||||||
- `api/src/modules/influence/campaign-emails/campaign-emails.service.ts` — added achievements.checkAndUnlock after email creation
|
|
||||||
- `api/src/modules/social/friendship.service.ts` — added achievements.checkAndUnlock on friend accept (both users)
|
|
||||||
- `admin/src/types/social.ts` — added AchievementDef, AchievementWithProgress, VolunteerStats, LeaderboardEntry interfaces
|
|
||||||
- `admin/src/pages/volunteer/AchievementsPage.tsx` — badge gallery (locked/unlocked + progress bars), volunteer stats summary, leaderboard (canvass/shifts/campaigns tabs)
|
|
||||||
- `admin/src/pages/volunteer/SocialProfilePage.tsx` — added achievement badges section to own + other user profiles
|
|
||||||
- `admin/src/pages/volunteer/SocialFeedPage.tsx` — added top canvassers leaderboard widget
|
|
||||||
- `admin/src/App.tsx` — added `/volunteer/achievements` route
|
|
||||||
|
|
||||||
### Phase 15 — Real-Time Features (SSE for Live Notifications, Online Status)
|
|
||||||
- `api/src/modules/social/sse.service.ts` — in-memory SSE connection manager (addClient, removeClient, sendToUser, sendToUsers, heartbeat, closeAll)
|
|
||||||
- `api/src/modules/social/presence.service.ts` — online/offline tracking with privacy filtering, broadcastPresence to friends, stale cleanup (5min timeout), markAllOffline (startup)
|
|
||||||
- `api/src/modules/social/sse.routes.ts` — GET `/` (SSE stream), GET `/online-friends`, GET `/status`
|
|
||||||
- `api/src/modules/social/social.routes.ts` — mounted sseRouter at `/sse`, added query-param token injection for EventSource auth
|
|
||||||
- `api/src/modules/social/notification.service.ts` — SSE push after notification creation (real-time delivery)
|
|
||||||
- `api/src/modules/social/friendship.service.ts` — SSE push on friend_request + friend_accepted events
|
|
||||||
- `api/src/modules/social/poke.service.ts` — SSE push on poke events
|
|
||||||
- `api/src/server.ts` — SSE heartbeat start, presenceService.markAllOffline on startup, 1-min stale cleanup interval, sseService.closeAll on graceful shutdown
|
|
||||||
- `admin/src/hooks/useSSE.ts` — EventSource hook with auto-reconnect (exponential backoff), handles notification/presence/friend_request/friend_accepted/poke events
|
|
||||||
- `admin/src/components/social/OnlineIndicator.tsx` — green dot showing online status for friends
|
|
||||||
- `admin/src/components/social/UserAvatar.tsx` — added showOnline prop with OnlineIndicator overlay
|
|
||||||
- `admin/src/stores/social.store.ts` — added onlineFriends state + fetchOnlineFriends action
|
|
||||||
- `admin/src/components/VolunteerLayout.tsx` — initialized useSSE() on mount
|
|
||||||
- `admin/src/components/social/NotificationBell.tsx` — reduced polling to 2-min fallback (SSE handles real-time)
|
|
||||||
- `admin/src/pages/volunteer/FriendsPage.tsx` — enabled showOnline on friend list avatars
|
|
||||||
- `admin/src/pages/volunteer/SocialProfilePage.tsx` — enabled showOnline on other user profile avatars
|
|
||||||
@ -713,6 +713,7 @@ function SystemUpgradeTab() {
|
|||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
const [skipBackup, setSkipBackup] = useState(false);
|
const [skipBackup, setSkipBackup] = useState(false);
|
||||||
const [pullServices, setPullServices] = useState(false);
|
const [pullServices, setPullServices] = useState(false);
|
||||||
|
const [useRegistry, setUseRegistry] = useState(false);
|
||||||
const [history, setHistory] = useState<UpgradeResult[]>([]);
|
const [history, setHistory] = useState<UpgradeResult[]>([]);
|
||||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const checkStartRef = useRef<string | null>(null);
|
const checkStartRef = useRef<string | null>(null);
|
||||||
@ -818,7 +819,7 @@ function SystemUpgradeTab() {
|
|||||||
setUpgrading(true);
|
setUpgrading(true);
|
||||||
setResult(null);
|
setResult(null);
|
||||||
try {
|
try {
|
||||||
await api.post('/upgrade/start', { skipBackup, pullServices });
|
await api.post('/upgrade/start', { skipBackup, pullServices, useRegistry });
|
||||||
startUpgradePoll();
|
startUpgradePoll();
|
||||||
} catch {
|
} catch {
|
||||||
setUpgrading(false);
|
setUpgrading(false);
|
||||||
@ -1125,6 +1126,40 @@ function SystemUpgradeTab() {
|
|||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Container Registry */}
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
title={<Space><CloudSyncOutlined /> Container Registry</Space>}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Switch
|
||||||
|
checked={settings?.useRegistryForUpgrade ?? false}
|
||||||
|
onChange={(v) => handleAutoUpgradeToggle('useRegistryForUpgrade', v)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Text>Use Gitea registry for upgrades</Text>
|
||||||
|
<br />
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
Pull pre-built production images instead of compiling from source. Faster upgrades.
|
||||||
|
Requires <Text code style={{ fontSize: 12 }}>scripts/build-and-push.sh</Text> to have been run first.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{settings?.useRegistryForUpgrade && (
|
||||||
|
<div>
|
||||||
|
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 2 }}>
|
||||||
|
Registry URL
|
||||||
|
</Text>
|
||||||
|
<Text code style={{ fontSize: 12 }}>
|
||||||
|
{settings?.giteaRegistryUrl || 'gitea.bnkops.com/admin'}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Recent Commits Feed */}
|
{/* Recent Commits Feed */}
|
||||||
{status?.changelog && status.changelog.length > 0 && status.commitsBehind === 0 && (
|
{status?.changelog && status.changelog.length > 0 && status.commitsBehind === 0 && (
|
||||||
<Card
|
<Card
|
||||||
@ -1258,6 +1293,12 @@ function SystemUpgradeTab() {
|
|||||||
>
|
>
|
||||||
<Text type="danger">Skip backup (not recommended)</Text>
|
<Text type="danger">Skip backup (not recommended)</Text>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
<Checkbox
|
||||||
|
checked={useRegistry}
|
||||||
|
onChange={(e) => setUseRegistry(e.target.checked)}
|
||||||
|
>
|
||||||
|
Use registry images (faster — needs build-and-push.sh run first)
|
||||||
|
</Checkbox>
|
||||||
</Space>
|
</Space>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1184,6 +1184,9 @@ export interface SiteSettings {
|
|||||||
autoUpgradeSchedule?: 'daily-3am' | 'daily-4am' | 'daily-5am' | 'weekly-sun-3am' | 'weekly-mon-3am' | '12h' | '24h';
|
autoUpgradeSchedule?: 'daily-3am' | 'daily-4am' | 'daily-5am' | 'weekly-sun-3am' | 'weekly-mon-3am' | '12h' | '24h';
|
||||||
autoUpgradePullServices?: boolean;
|
autoUpgradePullServices?: boolean;
|
||||||
notifyAdminAutoUpgrade?: boolean;
|
notifyAdminAutoUpgrade?: boolean;
|
||||||
|
// Registry settings
|
||||||
|
useRegistryForUpgrade?: boolean;
|
||||||
|
giteaRegistryUrl?: string;
|
||||||
// Navigation configuration
|
// Navigation configuration
|
||||||
navConfig: NavConfig | null;
|
navConfig: NavConfig | null;
|
||||||
// User Provisioning
|
// User Provisioning
|
||||||
|
|||||||
@ -25,10 +25,13 @@ RUN npm run build
|
|||||||
# Production stage
|
# Production stage
|
||||||
FROM node:22-alpine AS production
|
FROM node:22-alpine AS production
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
# Copy compiled output and manifests
|
||||||
COPY --from=build /app/dist ./dist
|
COPY --from=build /app/dist ./dist
|
||||||
COPY --from=build /app/node_modules ./node_modules
|
|
||||||
COPY --from=build /app/package.json ./
|
COPY --from=build /app/package.json ./
|
||||||
|
COPY --from=build /app/package-lock.json* ./
|
||||||
COPY --from=build /app/prisma ./prisma
|
COPY --from=build /app/prisma ./prisma
|
||||||
|
# Install production-only deps and regenerate Prisma client
|
||||||
|
RUN npm ci --omit=dev && npx prisma generate
|
||||||
COPY --from=build /app/docker-entrypoint.sh /usr/local/bin/
|
COPY --from=build /app/docker-entrypoint.sh /usr/local/bin/
|
||||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh \
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh \
|
||||||
&& mkdir -p /app/uploads && chown -R node:node /app/uploads
|
&& mkdir -p /app/uploads && chown -R node:node /app/uploads
|
||||||
|
|||||||
@ -40,11 +40,15 @@ WORKDIR /app
|
|||||||
# Install ffmpeg for video metadata, vips-dev for sharp HEIC support, yt-dlp for video fetching
|
# Install ffmpeg for video metadata, vips-dev for sharp HEIC support, yt-dlp for video fetching
|
||||||
RUN apk add --no-cache ffmpeg vips-dev python3 py3-pip && pip3 install --break-system-packages yt-dlp
|
RUN apk add --no-cache ffmpeg vips-dev python3 py3-pip && pip3 install --break-system-packages yt-dlp
|
||||||
|
|
||||||
# Copy built files and node_modules
|
# Copy manifests and install production-only deps (no devDeps like typescript)
|
||||||
|
COPY --from=build /app/package*.json ./
|
||||||
|
RUN npm ci --omit=dev --ignore-scripts && \
|
||||||
|
npm install --no-save @img/sharp-linuxmusl-x64
|
||||||
|
|
||||||
|
# Copy built output and schema
|
||||||
COPY --from=build /app/dist ./dist
|
COPY --from=build /app/dist ./dist
|
||||||
COPY --from=build /app/node_modules ./node_modules
|
|
||||||
COPY --from=build /app/package.json ./
|
|
||||||
COPY --from=build /app/prisma ./prisma
|
COPY --from=build /app/prisma ./prisma
|
||||||
|
RUN npx prisma generate
|
||||||
|
|
||||||
# Run as non-root user
|
# Run as non-root user
|
||||||
USER node
|
USER node
|
||||||
|
|||||||
198
api/dist/config/env.d.ts
vendored
198
api/dist/config/env.d.ts
vendored
@ -5,15 +5,19 @@ declare const envSchema: z.ZodObject<{
|
|||||||
API_URL: z.ZodDefault<z.ZodString>;
|
API_URL: z.ZodDefault<z.ZodString>;
|
||||||
ADMIN_URL: z.ZodDefault<z.ZodString>;
|
ADMIN_URL: z.ZodDefault<z.ZodString>;
|
||||||
DOMAIN: z.ZodDefault<z.ZodString>;
|
DOMAIN: z.ZodDefault<z.ZodString>;
|
||||||
|
INSTANCE_LABEL: z.ZodDefault<z.ZodString>;
|
||||||
|
BUNKER_OPS_ENABLED: z.ZodDefault<z.ZodString>;
|
||||||
|
BUNKER_OPS_REMOTE_WRITE_URL: z.ZodDefault<z.ZodString>;
|
||||||
DATABASE_URL: z.ZodString;
|
DATABASE_URL: z.ZodString;
|
||||||
REDIS_URL: z.ZodDefault<z.ZodString>;
|
REDIS_URL: z.ZodDefault<z.ZodString>;
|
||||||
JWT_ACCESS_SECRET: z.ZodString;
|
JWT_ACCESS_SECRET: z.ZodString;
|
||||||
JWT_REFRESH_SECRET: z.ZodString;
|
JWT_REFRESH_SECRET: z.ZodString;
|
||||||
|
JWT_INVITE_SECRET: z.ZodString;
|
||||||
JWT_ACCESS_EXPIRY: z.ZodDefault<z.ZodString>;
|
JWT_ACCESS_EXPIRY: z.ZodDefault<z.ZodString>;
|
||||||
JWT_REFRESH_EXPIRY: z.ZodDefault<z.ZodString>;
|
JWT_REFRESH_EXPIRY: z.ZodDefault<z.ZodString>;
|
||||||
ENCRYPTION_KEY: z.ZodOptional<z.ZodString>;
|
ENCRYPTION_KEY: z.ZodOptional<z.ZodString>;
|
||||||
INITIAL_ADMIN_EMAIL: z.ZodDefault<z.ZodString>;
|
INITIAL_ADMIN_EMAIL: z.ZodDefault<z.ZodString>;
|
||||||
INITIAL_ADMIN_PASSWORD: z.ZodDefault<z.ZodString>;
|
INITIAL_ADMIN_PASSWORD: z.ZodEffects<z.ZodDefault<z.ZodString>, string, string | undefined>;
|
||||||
SMTP_HOST: z.ZodDefault<z.ZodString>;
|
SMTP_HOST: z.ZodDefault<z.ZodString>;
|
||||||
SMTP_PORT: z.ZodDefault<z.ZodNumber>;
|
SMTP_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
SMTP_USER: z.ZodDefault<z.ZodString>;
|
SMTP_USER: z.ZodDefault<z.ZodString>;
|
||||||
@ -26,6 +30,7 @@ declare const envSchema: z.ZodObject<{
|
|||||||
LISTMONK_ADMIN_USER: z.ZodDefault<z.ZodString>;
|
LISTMONK_ADMIN_USER: z.ZodDefault<z.ZodString>;
|
||||||
LISTMONK_ADMIN_PASSWORD: z.ZodDefault<z.ZodString>;
|
LISTMONK_ADMIN_PASSWORD: z.ZodDefault<z.ZodString>;
|
||||||
LISTMONK_SYNC_ENABLED: z.ZodDefault<z.ZodString>;
|
LISTMONK_SYNC_ENABLED: z.ZodDefault<z.ZodString>;
|
||||||
|
LISTMONK_WEBHOOK_SECRET: z.ZodDefault<z.ZodString>;
|
||||||
LISTMONK_PROXY_PORT: z.ZodDefault<z.ZodNumber>;
|
LISTMONK_PROXY_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
REPRESENT_API_URL: z.ZodDefault<z.ZodString>;
|
REPRESENT_API_URL: z.ZodDefault<z.ZodString>;
|
||||||
CORS_ORIGINS: z.ZodDefault<z.ZodString>;
|
CORS_ORIGINS: z.ZodDefault<z.ZodString>;
|
||||||
@ -58,6 +63,27 @@ declare const envSchema: z.ZodObject<{
|
|||||||
EXCALIDRAW_URL: z.ZodDefault<z.ZodString>;
|
EXCALIDRAW_URL: z.ZodDefault<z.ZodString>;
|
||||||
EXCALIDRAW_PORT: z.ZodDefault<z.ZodNumber>;
|
EXCALIDRAW_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
EXCALIDRAW_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
EXCALIDRAW_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
|
HOMEPAGE_URL: z.ZodDefault<z.ZodString>;
|
||||||
|
HOMEPAGE_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
|
VAULTWARDEN_URL: z.ZodDefault<z.ZodString>;
|
||||||
|
VAULTWARDEN_ADMIN_TOKEN: z.ZodDefault<z.ZodString>;
|
||||||
|
VAULTWARDEN_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
|
ROCKETCHAT_URL: z.ZodDefault<z.ZodString>;
|
||||||
|
ROCKETCHAT_ADMIN_USER: z.ZodDefault<z.ZodString>;
|
||||||
|
ROCKETCHAT_ADMIN_PASSWORD: z.ZodDefault<z.ZodString>;
|
||||||
|
ROCKETCHAT_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
|
ENABLE_CHAT: z.ZodDefault<z.ZodString>;
|
||||||
|
GANCIO_URL: z.ZodDefault<z.ZodString>;
|
||||||
|
GANCIO_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
|
GANCIO_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
|
GANCIO_ADMIN_USER: z.ZodDefault<z.ZodString>;
|
||||||
|
GANCIO_ADMIN_PASSWORD: z.ZodDefault<z.ZodString>;
|
||||||
|
GANCIO_SYNC_ENABLED: z.ZodDefault<z.ZodString>;
|
||||||
|
ENABLE_MEET: z.ZodDefault<z.ZodString>;
|
||||||
|
JITSI_APP_ID: z.ZodDefault<z.ZodString>;
|
||||||
|
JITSI_APP_SECRET: z.ZodDefault<z.ZodString>;
|
||||||
|
JITSI_URL: z.ZodDefault<z.ZodString>;
|
||||||
|
JITSI_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
PANGOLIN_API_URL: z.ZodEffects<z.ZodDefault<z.ZodString>, string, string | undefined>;
|
PANGOLIN_API_URL: z.ZodEffects<z.ZodDefault<z.ZodString>, string, string | undefined>;
|
||||||
PANGOLIN_API_KEY: z.ZodDefault<z.ZodString>;
|
PANGOLIN_API_KEY: z.ZodDefault<z.ZodString>;
|
||||||
PANGOLIN_ORG_ID: z.ZodDefault<z.ZodString>;
|
PANGOLIN_ORG_ID: z.ZodDefault<z.ZodString>;
|
||||||
@ -66,13 +92,32 @@ declare const envSchema: z.ZodObject<{
|
|||||||
PANGOLIN_NEWT_ID: z.ZodDefault<z.ZodString>;
|
PANGOLIN_NEWT_ID: z.ZodDefault<z.ZodString>;
|
||||||
PANGOLIN_NEWT_SECRET: z.ZodDefault<z.ZodString>;
|
PANGOLIN_NEWT_SECRET: z.ZodDefault<z.ZodString>;
|
||||||
NAR_DATA_DIR: z.ZodDefault<z.ZodString>;
|
NAR_DATA_DIR: z.ZodDefault<z.ZodString>;
|
||||||
|
OVERPASS_API_URL: z.ZodDefault<z.ZodString>;
|
||||||
|
OVERPASS_MIN_DELAY_MS: z.ZodDefault<z.ZodNumber>;
|
||||||
|
AREA_IMPORT_MAX_GRID_POINTS: z.ZodDefault<z.ZodNumber>;
|
||||||
|
ENABLE_PAYMENTS: z.ZodDefault<z.ZodString>;
|
||||||
ENABLE_MEDIA_FEATURES: z.ZodDefault<z.ZodString>;
|
ENABLE_MEDIA_FEATURES: z.ZodDefault<z.ZodString>;
|
||||||
MEDIA_API_PORT: z.ZodDefault<z.ZodNumber>;
|
MEDIA_API_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
MEDIA_API_PUBLIC_URL: z.ZodDefault<z.ZodString>;
|
MEDIA_API_PUBLIC_URL: z.ZodDefault<z.ZodString>;
|
||||||
MEDIA_ROOT: z.ZodDefault<z.ZodString>;
|
MEDIA_ROOT: z.ZodDefault<z.ZodString>;
|
||||||
MEDIA_UPLOADS: z.ZodDefault<z.ZodString>;
|
MEDIA_UPLOADS: z.ZodDefault<z.ZodString>;
|
||||||
MAX_UPLOAD_SIZE_GB: z.ZodDefault<z.ZodNumber>;
|
MAX_UPLOAD_SIZE_GB: z.ZodDefault<z.ZodNumber>;
|
||||||
PUBLIC_MEDIA_PORT: z.ZodDefault<z.ZodNumber>;
|
GITEA_REGISTRY: z.ZodDefault<z.ZodString>;
|
||||||
|
GITEA_REGISTRY_USER: z.ZodDefault<z.ZodString>;
|
||||||
|
GITEA_REGISTRY_PASS: z.ZodDefault<z.ZodString>;
|
||||||
|
GITEA_COMMENTS_ENABLED: z.ZodDefault<z.ZodString>;
|
||||||
|
GITEA_API_TOKEN: z.ZodDefault<z.ZodString>;
|
||||||
|
GITEA_COMMENTS_REPO_OWNER: z.ZodDefault<z.ZodString>;
|
||||||
|
GITEA_COMMENTS_REPO_NAME: z.ZodDefault<z.ZodString>;
|
||||||
|
GITEA_OAUTH_CLIENT_ID: z.ZodDefault<z.ZodString>;
|
||||||
|
GITEA_OAUTH_CLIENT_SECRET: z.ZodDefault<z.ZodString>;
|
||||||
|
ENABLE_SMS: z.ZodDefault<z.ZodString>;
|
||||||
|
TERMUX_API_URL: z.ZodDefault<z.ZodString>;
|
||||||
|
TERMUX_API_KEY: z.ZodDefault<z.ZodString>;
|
||||||
|
SMS_DELAY_BETWEEN_MS: z.ZodDefault<z.ZodNumber>;
|
||||||
|
SMS_MAX_RETRIES: z.ZodDefault<z.ZodNumber>;
|
||||||
|
SMS_RESPONSE_SYNC_INTERVAL_MS: z.ZodDefault<z.ZodNumber>;
|
||||||
|
SMS_DEVICE_MONITOR_INTERVAL_MS: z.ZodDefault<z.ZodNumber>;
|
||||||
CODE_SERVER_URL: z.ZodDefault<z.ZodString>;
|
CODE_SERVER_URL: z.ZodDefault<z.ZodString>;
|
||||||
CODE_SERVER_PORT: z.ZodDefault<z.ZodNumber>;
|
CODE_SERVER_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
MKDOCS_PREVIEW_URL: z.ZodDefault<z.ZodString>;
|
MKDOCS_PREVIEW_URL: z.ZodDefault<z.ZodString>;
|
||||||
@ -86,8 +131,10 @@ declare const envSchema: z.ZodObject<{
|
|||||||
PROMETHEUS_PORT: z.ZodDefault<z.ZodNumber>;
|
PROMETHEUS_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
GRAFANA_URL: z.ZodDefault<z.ZodString>;
|
GRAFANA_URL: z.ZodDefault<z.ZodString>;
|
||||||
GRAFANA_PORT: z.ZodDefault<z.ZodNumber>;
|
GRAFANA_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
|
GRAFANA_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
ALERTMANAGER_URL: z.ZodDefault<z.ZodString>;
|
ALERTMANAGER_URL: z.ZodDefault<z.ZodString>;
|
||||||
ALERTMANAGER_PORT: z.ZodDefault<z.ZodNumber>;
|
ALERTMANAGER_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
|
ALERTMANAGER_EMBED_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
CADVISOR_URL: z.ZodDefault<z.ZodString>;
|
CADVISOR_URL: z.ZodDefault<z.ZodString>;
|
||||||
CADVISOR_PORT: z.ZodDefault<z.ZodNumber>;
|
CADVISOR_PORT: z.ZodDefault<z.ZodNumber>;
|
||||||
NODE_EXPORTER_URL: z.ZodDefault<z.ZodString>;
|
NODE_EXPORTER_URL: z.ZodDefault<z.ZodString>;
|
||||||
@ -102,10 +149,14 @@ declare const envSchema: z.ZodObject<{
|
|||||||
API_URL: string;
|
API_URL: string;
|
||||||
ADMIN_URL: string;
|
ADMIN_URL: string;
|
||||||
DOMAIN: string;
|
DOMAIN: string;
|
||||||
|
INSTANCE_LABEL: string;
|
||||||
|
BUNKER_OPS_ENABLED: string;
|
||||||
|
BUNKER_OPS_REMOTE_WRITE_URL: string;
|
||||||
DATABASE_URL: string;
|
DATABASE_URL: string;
|
||||||
REDIS_URL: string;
|
REDIS_URL: string;
|
||||||
JWT_ACCESS_SECRET: string;
|
JWT_ACCESS_SECRET: string;
|
||||||
JWT_REFRESH_SECRET: string;
|
JWT_REFRESH_SECRET: string;
|
||||||
|
JWT_INVITE_SECRET: string;
|
||||||
JWT_ACCESS_EXPIRY: string;
|
JWT_ACCESS_EXPIRY: string;
|
||||||
JWT_REFRESH_EXPIRY: string;
|
JWT_REFRESH_EXPIRY: string;
|
||||||
INITIAL_ADMIN_EMAIL: string;
|
INITIAL_ADMIN_EMAIL: string;
|
||||||
@ -122,6 +173,7 @@ declare const envSchema: z.ZodObject<{
|
|||||||
LISTMONK_ADMIN_USER: string;
|
LISTMONK_ADMIN_USER: string;
|
||||||
LISTMONK_ADMIN_PASSWORD: string;
|
LISTMONK_ADMIN_PASSWORD: string;
|
||||||
LISTMONK_SYNC_ENABLED: string;
|
LISTMONK_SYNC_ENABLED: string;
|
||||||
|
LISTMONK_WEBHOOK_SECRET: string;
|
||||||
LISTMONK_PROXY_PORT: number;
|
LISTMONK_PROXY_PORT: number;
|
||||||
REPRESENT_API_URL: string;
|
REPRESENT_API_URL: string;
|
||||||
CORS_ORIGINS: string;
|
CORS_ORIGINS: string;
|
||||||
@ -152,6 +204,27 @@ declare const envSchema: z.ZodObject<{
|
|||||||
EXCALIDRAW_URL: string;
|
EXCALIDRAW_URL: string;
|
||||||
EXCALIDRAW_PORT: number;
|
EXCALIDRAW_PORT: number;
|
||||||
EXCALIDRAW_EMBED_PORT: number;
|
EXCALIDRAW_EMBED_PORT: number;
|
||||||
|
HOMEPAGE_URL: string;
|
||||||
|
HOMEPAGE_EMBED_PORT: number;
|
||||||
|
VAULTWARDEN_URL: string;
|
||||||
|
VAULTWARDEN_ADMIN_TOKEN: string;
|
||||||
|
VAULTWARDEN_EMBED_PORT: number;
|
||||||
|
ROCKETCHAT_URL: string;
|
||||||
|
ROCKETCHAT_ADMIN_USER: string;
|
||||||
|
ROCKETCHAT_ADMIN_PASSWORD: string;
|
||||||
|
ROCKETCHAT_EMBED_PORT: number;
|
||||||
|
ENABLE_CHAT: string;
|
||||||
|
GANCIO_URL: string;
|
||||||
|
GANCIO_PORT: number;
|
||||||
|
GANCIO_EMBED_PORT: number;
|
||||||
|
GANCIO_ADMIN_USER: string;
|
||||||
|
GANCIO_ADMIN_PASSWORD: string;
|
||||||
|
GANCIO_SYNC_ENABLED: string;
|
||||||
|
ENABLE_MEET: string;
|
||||||
|
JITSI_APP_ID: string;
|
||||||
|
JITSI_APP_SECRET: string;
|
||||||
|
JITSI_URL: string;
|
||||||
|
JITSI_EMBED_PORT: number;
|
||||||
PANGOLIN_API_URL: string;
|
PANGOLIN_API_URL: string;
|
||||||
PANGOLIN_API_KEY: string;
|
PANGOLIN_API_KEY: string;
|
||||||
PANGOLIN_ORG_ID: string;
|
PANGOLIN_ORG_ID: string;
|
||||||
@ -160,13 +233,32 @@ declare const envSchema: z.ZodObject<{
|
|||||||
PANGOLIN_NEWT_ID: string;
|
PANGOLIN_NEWT_ID: string;
|
||||||
PANGOLIN_NEWT_SECRET: string;
|
PANGOLIN_NEWT_SECRET: string;
|
||||||
NAR_DATA_DIR: string;
|
NAR_DATA_DIR: string;
|
||||||
|
OVERPASS_API_URL: string;
|
||||||
|
OVERPASS_MIN_DELAY_MS: number;
|
||||||
|
AREA_IMPORT_MAX_GRID_POINTS: number;
|
||||||
|
ENABLE_PAYMENTS: string;
|
||||||
ENABLE_MEDIA_FEATURES: string;
|
ENABLE_MEDIA_FEATURES: string;
|
||||||
MEDIA_API_PORT: number;
|
MEDIA_API_PORT: number;
|
||||||
MEDIA_API_PUBLIC_URL: string;
|
MEDIA_API_PUBLIC_URL: string;
|
||||||
MEDIA_ROOT: string;
|
MEDIA_ROOT: string;
|
||||||
MEDIA_UPLOADS: string;
|
MEDIA_UPLOADS: string;
|
||||||
MAX_UPLOAD_SIZE_GB: number;
|
MAX_UPLOAD_SIZE_GB: number;
|
||||||
PUBLIC_MEDIA_PORT: number;
|
GITEA_REGISTRY: string;
|
||||||
|
GITEA_REGISTRY_USER: string;
|
||||||
|
GITEA_REGISTRY_PASS: string;
|
||||||
|
GITEA_COMMENTS_ENABLED: string;
|
||||||
|
GITEA_API_TOKEN: string;
|
||||||
|
GITEA_COMMENTS_REPO_OWNER: string;
|
||||||
|
GITEA_COMMENTS_REPO_NAME: string;
|
||||||
|
GITEA_OAUTH_CLIENT_ID: string;
|
||||||
|
GITEA_OAUTH_CLIENT_SECRET: string;
|
||||||
|
ENABLE_SMS: string;
|
||||||
|
TERMUX_API_URL: string;
|
||||||
|
TERMUX_API_KEY: string;
|
||||||
|
SMS_DELAY_BETWEEN_MS: number;
|
||||||
|
SMS_MAX_RETRIES: number;
|
||||||
|
SMS_RESPONSE_SYNC_INTERVAL_MS: number;
|
||||||
|
SMS_DEVICE_MONITOR_INTERVAL_MS: number;
|
||||||
CODE_SERVER_URL: string;
|
CODE_SERVER_URL: string;
|
||||||
CODE_SERVER_PORT: number;
|
CODE_SERVER_PORT: number;
|
||||||
MKDOCS_PREVIEW_URL: string;
|
MKDOCS_PREVIEW_URL: string;
|
||||||
@ -180,8 +272,10 @@ declare const envSchema: z.ZodObject<{
|
|||||||
PROMETHEUS_PORT: number;
|
PROMETHEUS_PORT: number;
|
||||||
GRAFANA_URL: string;
|
GRAFANA_URL: string;
|
||||||
GRAFANA_PORT: number;
|
GRAFANA_PORT: number;
|
||||||
|
GRAFANA_EMBED_PORT: number;
|
||||||
ALERTMANAGER_URL: string;
|
ALERTMANAGER_URL: string;
|
||||||
ALERTMANAGER_PORT: number;
|
ALERTMANAGER_PORT: number;
|
||||||
|
ALERTMANAGER_EMBED_PORT: number;
|
||||||
CADVISOR_URL: string;
|
CADVISOR_URL: string;
|
||||||
CADVISOR_PORT: number;
|
CADVISOR_PORT: number;
|
||||||
NODE_EXPORTER_URL: string;
|
NODE_EXPORTER_URL: string;
|
||||||
@ -197,11 +291,15 @@ declare const envSchema: z.ZodObject<{
|
|||||||
DATABASE_URL: string;
|
DATABASE_URL: string;
|
||||||
JWT_ACCESS_SECRET: string;
|
JWT_ACCESS_SECRET: string;
|
||||||
JWT_REFRESH_SECRET: string;
|
JWT_REFRESH_SECRET: string;
|
||||||
|
JWT_INVITE_SECRET: string;
|
||||||
NODE_ENV?: "development" | "production" | "test" | undefined;
|
NODE_ENV?: "development" | "production" | "test" | undefined;
|
||||||
PORT?: number | undefined;
|
PORT?: number | undefined;
|
||||||
API_URL?: string | undefined;
|
API_URL?: string | undefined;
|
||||||
ADMIN_URL?: string | undefined;
|
ADMIN_URL?: string | undefined;
|
||||||
DOMAIN?: string | undefined;
|
DOMAIN?: string | undefined;
|
||||||
|
INSTANCE_LABEL?: string | undefined;
|
||||||
|
BUNKER_OPS_ENABLED?: string | undefined;
|
||||||
|
BUNKER_OPS_REMOTE_WRITE_URL?: string | undefined;
|
||||||
REDIS_URL?: string | undefined;
|
REDIS_URL?: string | undefined;
|
||||||
JWT_ACCESS_EXPIRY?: string | undefined;
|
JWT_ACCESS_EXPIRY?: string | undefined;
|
||||||
JWT_REFRESH_EXPIRY?: string | undefined;
|
JWT_REFRESH_EXPIRY?: string | undefined;
|
||||||
@ -220,6 +318,7 @@ declare const envSchema: z.ZodObject<{
|
|||||||
LISTMONK_ADMIN_USER?: string | undefined;
|
LISTMONK_ADMIN_USER?: string | undefined;
|
||||||
LISTMONK_ADMIN_PASSWORD?: string | undefined;
|
LISTMONK_ADMIN_PASSWORD?: string | undefined;
|
||||||
LISTMONK_SYNC_ENABLED?: string | undefined;
|
LISTMONK_SYNC_ENABLED?: string | undefined;
|
||||||
|
LISTMONK_WEBHOOK_SECRET?: string | undefined;
|
||||||
LISTMONK_PROXY_PORT?: number | undefined;
|
LISTMONK_PROXY_PORT?: number | undefined;
|
||||||
REPRESENT_API_URL?: string | undefined;
|
REPRESENT_API_URL?: string | undefined;
|
||||||
CORS_ORIGINS?: string | undefined;
|
CORS_ORIGINS?: string | undefined;
|
||||||
@ -252,6 +351,27 @@ declare const envSchema: z.ZodObject<{
|
|||||||
EXCALIDRAW_URL?: string | undefined;
|
EXCALIDRAW_URL?: string | undefined;
|
||||||
EXCALIDRAW_PORT?: number | undefined;
|
EXCALIDRAW_PORT?: number | undefined;
|
||||||
EXCALIDRAW_EMBED_PORT?: number | undefined;
|
EXCALIDRAW_EMBED_PORT?: number | undefined;
|
||||||
|
HOMEPAGE_URL?: string | undefined;
|
||||||
|
HOMEPAGE_EMBED_PORT?: number | undefined;
|
||||||
|
VAULTWARDEN_URL?: string | undefined;
|
||||||
|
VAULTWARDEN_ADMIN_TOKEN?: string | undefined;
|
||||||
|
VAULTWARDEN_EMBED_PORT?: number | undefined;
|
||||||
|
ROCKETCHAT_URL?: string | undefined;
|
||||||
|
ROCKETCHAT_ADMIN_USER?: string | undefined;
|
||||||
|
ROCKETCHAT_ADMIN_PASSWORD?: string | undefined;
|
||||||
|
ROCKETCHAT_EMBED_PORT?: number | undefined;
|
||||||
|
ENABLE_CHAT?: string | undefined;
|
||||||
|
GANCIO_URL?: string | undefined;
|
||||||
|
GANCIO_PORT?: number | undefined;
|
||||||
|
GANCIO_EMBED_PORT?: number | undefined;
|
||||||
|
GANCIO_ADMIN_USER?: string | undefined;
|
||||||
|
GANCIO_ADMIN_PASSWORD?: string | undefined;
|
||||||
|
GANCIO_SYNC_ENABLED?: string | undefined;
|
||||||
|
ENABLE_MEET?: string | undefined;
|
||||||
|
JITSI_APP_ID?: string | undefined;
|
||||||
|
JITSI_APP_SECRET?: string | undefined;
|
||||||
|
JITSI_URL?: string | undefined;
|
||||||
|
JITSI_EMBED_PORT?: number | undefined;
|
||||||
PANGOLIN_API_URL?: string | undefined;
|
PANGOLIN_API_URL?: string | undefined;
|
||||||
PANGOLIN_API_KEY?: string | undefined;
|
PANGOLIN_API_KEY?: string | undefined;
|
||||||
PANGOLIN_ORG_ID?: string | undefined;
|
PANGOLIN_ORG_ID?: string | undefined;
|
||||||
@ -260,13 +380,32 @@ declare const envSchema: z.ZodObject<{
|
|||||||
PANGOLIN_NEWT_ID?: string | undefined;
|
PANGOLIN_NEWT_ID?: string | undefined;
|
||||||
PANGOLIN_NEWT_SECRET?: string | undefined;
|
PANGOLIN_NEWT_SECRET?: string | undefined;
|
||||||
NAR_DATA_DIR?: string | undefined;
|
NAR_DATA_DIR?: string | undefined;
|
||||||
|
OVERPASS_API_URL?: string | undefined;
|
||||||
|
OVERPASS_MIN_DELAY_MS?: number | undefined;
|
||||||
|
AREA_IMPORT_MAX_GRID_POINTS?: number | undefined;
|
||||||
|
ENABLE_PAYMENTS?: string | undefined;
|
||||||
ENABLE_MEDIA_FEATURES?: string | undefined;
|
ENABLE_MEDIA_FEATURES?: string | undefined;
|
||||||
MEDIA_API_PORT?: number | undefined;
|
MEDIA_API_PORT?: number | undefined;
|
||||||
MEDIA_API_PUBLIC_URL?: string | undefined;
|
MEDIA_API_PUBLIC_URL?: string | undefined;
|
||||||
MEDIA_ROOT?: string | undefined;
|
MEDIA_ROOT?: string | undefined;
|
||||||
MEDIA_UPLOADS?: string | undefined;
|
MEDIA_UPLOADS?: string | undefined;
|
||||||
MAX_UPLOAD_SIZE_GB?: number | undefined;
|
MAX_UPLOAD_SIZE_GB?: number | undefined;
|
||||||
PUBLIC_MEDIA_PORT?: number | undefined;
|
GITEA_REGISTRY?: string | undefined;
|
||||||
|
GITEA_REGISTRY_USER?: string | undefined;
|
||||||
|
GITEA_REGISTRY_PASS?: string | undefined;
|
||||||
|
GITEA_COMMENTS_ENABLED?: string | undefined;
|
||||||
|
GITEA_API_TOKEN?: string | undefined;
|
||||||
|
GITEA_COMMENTS_REPO_OWNER?: string | undefined;
|
||||||
|
GITEA_COMMENTS_REPO_NAME?: string | undefined;
|
||||||
|
GITEA_OAUTH_CLIENT_ID?: string | undefined;
|
||||||
|
GITEA_OAUTH_CLIENT_SECRET?: string | undefined;
|
||||||
|
ENABLE_SMS?: string | undefined;
|
||||||
|
TERMUX_API_URL?: string | undefined;
|
||||||
|
TERMUX_API_KEY?: string | undefined;
|
||||||
|
SMS_DELAY_BETWEEN_MS?: number | undefined;
|
||||||
|
SMS_MAX_RETRIES?: number | undefined;
|
||||||
|
SMS_RESPONSE_SYNC_INTERVAL_MS?: number | undefined;
|
||||||
|
SMS_DEVICE_MONITOR_INTERVAL_MS?: number | undefined;
|
||||||
CODE_SERVER_URL?: string | undefined;
|
CODE_SERVER_URL?: string | undefined;
|
||||||
CODE_SERVER_PORT?: number | undefined;
|
CODE_SERVER_PORT?: number | undefined;
|
||||||
MKDOCS_PREVIEW_URL?: string | undefined;
|
MKDOCS_PREVIEW_URL?: string | undefined;
|
||||||
@ -280,8 +419,10 @@ declare const envSchema: z.ZodObject<{
|
|||||||
PROMETHEUS_PORT?: number | undefined;
|
PROMETHEUS_PORT?: number | undefined;
|
||||||
GRAFANA_URL?: string | undefined;
|
GRAFANA_URL?: string | undefined;
|
||||||
GRAFANA_PORT?: number | undefined;
|
GRAFANA_PORT?: number | undefined;
|
||||||
|
GRAFANA_EMBED_PORT?: number | undefined;
|
||||||
ALERTMANAGER_URL?: string | undefined;
|
ALERTMANAGER_URL?: string | undefined;
|
||||||
ALERTMANAGER_PORT?: number | undefined;
|
ALERTMANAGER_PORT?: number | undefined;
|
||||||
|
ALERTMANAGER_EMBED_PORT?: number | undefined;
|
||||||
CADVISOR_URL?: string | undefined;
|
CADVISOR_URL?: string | undefined;
|
||||||
CADVISOR_PORT?: number | undefined;
|
CADVISOR_PORT?: number | undefined;
|
||||||
NODE_EXPORTER_URL?: string | undefined;
|
NODE_EXPORTER_URL?: string | undefined;
|
||||||
@ -298,10 +439,14 @@ export declare const env: {
|
|||||||
API_URL: string;
|
API_URL: string;
|
||||||
ADMIN_URL: string;
|
ADMIN_URL: string;
|
||||||
DOMAIN: string;
|
DOMAIN: string;
|
||||||
|
INSTANCE_LABEL: string;
|
||||||
|
BUNKER_OPS_ENABLED: string;
|
||||||
|
BUNKER_OPS_REMOTE_WRITE_URL: string;
|
||||||
DATABASE_URL: string;
|
DATABASE_URL: string;
|
||||||
REDIS_URL: string;
|
REDIS_URL: string;
|
||||||
JWT_ACCESS_SECRET: string;
|
JWT_ACCESS_SECRET: string;
|
||||||
JWT_REFRESH_SECRET: string;
|
JWT_REFRESH_SECRET: string;
|
||||||
|
JWT_INVITE_SECRET: string;
|
||||||
JWT_ACCESS_EXPIRY: string;
|
JWT_ACCESS_EXPIRY: string;
|
||||||
JWT_REFRESH_EXPIRY: string;
|
JWT_REFRESH_EXPIRY: string;
|
||||||
INITIAL_ADMIN_EMAIL: string;
|
INITIAL_ADMIN_EMAIL: string;
|
||||||
@ -318,6 +463,7 @@ export declare const env: {
|
|||||||
LISTMONK_ADMIN_USER: string;
|
LISTMONK_ADMIN_USER: string;
|
||||||
LISTMONK_ADMIN_PASSWORD: string;
|
LISTMONK_ADMIN_PASSWORD: string;
|
||||||
LISTMONK_SYNC_ENABLED: string;
|
LISTMONK_SYNC_ENABLED: string;
|
||||||
|
LISTMONK_WEBHOOK_SECRET: string;
|
||||||
LISTMONK_PROXY_PORT: number;
|
LISTMONK_PROXY_PORT: number;
|
||||||
REPRESENT_API_URL: string;
|
REPRESENT_API_URL: string;
|
||||||
CORS_ORIGINS: string;
|
CORS_ORIGINS: string;
|
||||||
@ -348,6 +494,27 @@ export declare const env: {
|
|||||||
EXCALIDRAW_URL: string;
|
EXCALIDRAW_URL: string;
|
||||||
EXCALIDRAW_PORT: number;
|
EXCALIDRAW_PORT: number;
|
||||||
EXCALIDRAW_EMBED_PORT: number;
|
EXCALIDRAW_EMBED_PORT: number;
|
||||||
|
HOMEPAGE_URL: string;
|
||||||
|
HOMEPAGE_EMBED_PORT: number;
|
||||||
|
VAULTWARDEN_URL: string;
|
||||||
|
VAULTWARDEN_ADMIN_TOKEN: string;
|
||||||
|
VAULTWARDEN_EMBED_PORT: number;
|
||||||
|
ROCKETCHAT_URL: string;
|
||||||
|
ROCKETCHAT_ADMIN_USER: string;
|
||||||
|
ROCKETCHAT_ADMIN_PASSWORD: string;
|
||||||
|
ROCKETCHAT_EMBED_PORT: number;
|
||||||
|
ENABLE_CHAT: string;
|
||||||
|
GANCIO_URL: string;
|
||||||
|
GANCIO_PORT: number;
|
||||||
|
GANCIO_EMBED_PORT: number;
|
||||||
|
GANCIO_ADMIN_USER: string;
|
||||||
|
GANCIO_ADMIN_PASSWORD: string;
|
||||||
|
GANCIO_SYNC_ENABLED: string;
|
||||||
|
ENABLE_MEET: string;
|
||||||
|
JITSI_APP_ID: string;
|
||||||
|
JITSI_APP_SECRET: string;
|
||||||
|
JITSI_URL: string;
|
||||||
|
JITSI_EMBED_PORT: number;
|
||||||
PANGOLIN_API_URL: string;
|
PANGOLIN_API_URL: string;
|
||||||
PANGOLIN_API_KEY: string;
|
PANGOLIN_API_KEY: string;
|
||||||
PANGOLIN_ORG_ID: string;
|
PANGOLIN_ORG_ID: string;
|
||||||
@ -356,13 +523,32 @@ export declare const env: {
|
|||||||
PANGOLIN_NEWT_ID: string;
|
PANGOLIN_NEWT_ID: string;
|
||||||
PANGOLIN_NEWT_SECRET: string;
|
PANGOLIN_NEWT_SECRET: string;
|
||||||
NAR_DATA_DIR: string;
|
NAR_DATA_DIR: string;
|
||||||
|
OVERPASS_API_URL: string;
|
||||||
|
OVERPASS_MIN_DELAY_MS: number;
|
||||||
|
AREA_IMPORT_MAX_GRID_POINTS: number;
|
||||||
|
ENABLE_PAYMENTS: string;
|
||||||
ENABLE_MEDIA_FEATURES: string;
|
ENABLE_MEDIA_FEATURES: string;
|
||||||
MEDIA_API_PORT: number;
|
MEDIA_API_PORT: number;
|
||||||
MEDIA_API_PUBLIC_URL: string;
|
MEDIA_API_PUBLIC_URL: string;
|
||||||
MEDIA_ROOT: string;
|
MEDIA_ROOT: string;
|
||||||
MEDIA_UPLOADS: string;
|
MEDIA_UPLOADS: string;
|
||||||
MAX_UPLOAD_SIZE_GB: number;
|
MAX_UPLOAD_SIZE_GB: number;
|
||||||
PUBLIC_MEDIA_PORT: number;
|
GITEA_REGISTRY: string;
|
||||||
|
GITEA_REGISTRY_USER: string;
|
||||||
|
GITEA_REGISTRY_PASS: string;
|
||||||
|
GITEA_COMMENTS_ENABLED: string;
|
||||||
|
GITEA_API_TOKEN: string;
|
||||||
|
GITEA_COMMENTS_REPO_OWNER: string;
|
||||||
|
GITEA_COMMENTS_REPO_NAME: string;
|
||||||
|
GITEA_OAUTH_CLIENT_ID: string;
|
||||||
|
GITEA_OAUTH_CLIENT_SECRET: string;
|
||||||
|
ENABLE_SMS: string;
|
||||||
|
TERMUX_API_URL: string;
|
||||||
|
TERMUX_API_KEY: string;
|
||||||
|
SMS_DELAY_BETWEEN_MS: number;
|
||||||
|
SMS_MAX_RETRIES: number;
|
||||||
|
SMS_RESPONSE_SYNC_INTERVAL_MS: number;
|
||||||
|
SMS_DEVICE_MONITOR_INTERVAL_MS: number;
|
||||||
CODE_SERVER_URL: string;
|
CODE_SERVER_URL: string;
|
||||||
CODE_SERVER_PORT: number;
|
CODE_SERVER_PORT: number;
|
||||||
MKDOCS_PREVIEW_URL: string;
|
MKDOCS_PREVIEW_URL: string;
|
||||||
@ -376,8 +562,10 @@ export declare const env: {
|
|||||||
PROMETHEUS_PORT: number;
|
PROMETHEUS_PORT: number;
|
||||||
GRAFANA_URL: string;
|
GRAFANA_URL: string;
|
||||||
GRAFANA_PORT: number;
|
GRAFANA_PORT: number;
|
||||||
|
GRAFANA_EMBED_PORT: number;
|
||||||
ALERTMANAGER_URL: string;
|
ALERTMANAGER_URL: string;
|
||||||
ALERTMANAGER_PORT: number;
|
ALERTMANAGER_PORT: number;
|
||||||
|
ALERTMANAGER_EMBED_PORT: number;
|
||||||
CADVISOR_URL: string;
|
CADVISOR_URL: string;
|
||||||
CADVISOR_PORT: number;
|
CADVISOR_PORT: number;
|
||||||
NODE_EXPORTER_URL: string;
|
NODE_EXPORTER_URL: string;
|
||||||
|
|||||||
2
api/dist/config/env.d.ts.map
vendored
2
api/dist/config/env.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/config/env.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,QAAA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkJb,CAAC;AAEH,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAY5C,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAgB,CAAC"}
|
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/config/env.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,QAAA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2Nb,CAAC;AAEH,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAY5C,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAgB,CAAC"}
|
||||||
65
api/dist/config/env.js
vendored
65
api/dist/config/env.js
vendored
@ -14,6 +14,10 @@ const envSchema = zod_1.z.object({
|
|||||||
API_URL: zod_1.z.string().default('http://localhost:4000'),
|
API_URL: zod_1.z.string().default('http://localhost:4000'),
|
||||||
ADMIN_URL: zod_1.z.string().default('http://localhost:3000'),
|
ADMIN_URL: zod_1.z.string().default('http://localhost:3000'),
|
||||||
DOMAIN: zod_1.z.string().default('cmlite.org'),
|
DOMAIN: zod_1.z.string().default('cmlite.org'),
|
||||||
|
// Bunker Ops (Fleet Management)
|
||||||
|
INSTANCE_LABEL: zod_1.z.string().default(''),
|
||||||
|
BUNKER_OPS_ENABLED: zod_1.z.string().default('false'),
|
||||||
|
BUNKER_OPS_REMOTE_WRITE_URL: zod_1.z.string().default(''),
|
||||||
// Database
|
// Database
|
||||||
DATABASE_URL: zod_1.z.string(),
|
DATABASE_URL: zod_1.z.string(),
|
||||||
// Redis
|
// Redis
|
||||||
@ -21,13 +25,15 @@ const envSchema = zod_1.z.object({
|
|||||||
// JWT
|
// JWT
|
||||||
JWT_ACCESS_SECRET: zod_1.z.string().min(32),
|
JWT_ACCESS_SECRET: zod_1.z.string().min(32),
|
||||||
JWT_REFRESH_SECRET: zod_1.z.string().min(32),
|
JWT_REFRESH_SECRET: zod_1.z.string().min(32),
|
||||||
|
JWT_INVITE_SECRET: zod_1.z.string().min(32),
|
||||||
JWT_ACCESS_EXPIRY: zod_1.z.string().default('15m'),
|
JWT_ACCESS_EXPIRY: zod_1.z.string().default('15m'),
|
||||||
JWT_REFRESH_EXPIRY: zod_1.z.string().default('7d'),
|
JWT_REFRESH_EXPIRY: zod_1.z.string().default('7d'),
|
||||||
// Encryption (for DB-stored secrets like SMTP password; falls back to JWT_ACCESS_SECRET)
|
// Encryption (for DB-stored secrets like SMTP password; falls back to JWT_ACCESS_SECRET)
|
||||||
ENCRYPTION_KEY: zod_1.z.string().optional(),
|
ENCRYPTION_KEY: zod_1.z.string().min(32, 'ENCRYPTION_KEY must be at least 32 characters').optional(),
|
||||||
// Initial Super Admin (auto-created during database seeding)
|
// Initial Super Admin (auto-created during database seeding)
|
||||||
INITIAL_ADMIN_EMAIL: zod_1.z.string().email().default('admin@cmlite.org'),
|
INITIAL_ADMIN_EMAIL: zod_1.z.string().email().default('admin@cmlite.org'),
|
||||||
INITIAL_ADMIN_PASSWORD: zod_1.z.string().min(12).default('REQUIRED_STRONG_PASSWORD_CHANGE_THIS'),
|
INITIAL_ADMIN_PASSWORD: zod_1.z.string().min(12).default('REQUIRED_STRONG_PASSWORD_CHANGE_THIS')
|
||||||
|
.refine((val) => val !== 'REQUIRED_STRONG_PASSWORD_CHANGE_THIS', { message: 'INITIAL_ADMIN_PASSWORD must be changed from the default placeholder value' }),
|
||||||
// SMTP
|
// SMTP
|
||||||
SMTP_HOST: zod_1.z.string().default('mailhog-changemaker'),
|
SMTP_HOST: zod_1.z.string().default('mailhog-changemaker'),
|
||||||
SMTP_PORT: zod_1.z.coerce.number().default(1025),
|
SMTP_PORT: zod_1.z.coerce.number().default(1025),
|
||||||
@ -42,6 +48,7 @@ const envSchema = zod_1.z.object({
|
|||||||
LISTMONK_ADMIN_USER: zod_1.z.string().default('admin'),
|
LISTMONK_ADMIN_USER: zod_1.z.string().default('admin'),
|
||||||
LISTMONK_ADMIN_PASSWORD: zod_1.z.string().default(''),
|
LISTMONK_ADMIN_PASSWORD: zod_1.z.string().default(''),
|
||||||
LISTMONK_SYNC_ENABLED: zod_1.z.string().default('false'),
|
LISTMONK_SYNC_ENABLED: zod_1.z.string().default('false'),
|
||||||
|
LISTMONK_WEBHOOK_SECRET: zod_1.z.string().default(''),
|
||||||
LISTMONK_PROXY_PORT: zod_1.z.coerce.number().default(9002),
|
LISTMONK_PROXY_PORT: zod_1.z.coerce.number().default(9002),
|
||||||
// Represent API (Canadian electoral data)
|
// Represent API (Canadian electoral data)
|
||||||
REPRESENT_API_URL: zod_1.z.string().default('https://represent.opennorth.ca'),
|
REPRESENT_API_URL: zod_1.z.string().default('https://represent.opennorth.ca'),
|
||||||
@ -84,6 +91,32 @@ const envSchema = zod_1.z.object({
|
|||||||
EXCALIDRAW_URL: zod_1.z.string().default('http://excalidraw-changemaker:80'),
|
EXCALIDRAW_URL: zod_1.z.string().default('http://excalidraw-changemaker:80'),
|
||||||
EXCALIDRAW_PORT: zod_1.z.coerce.number().default(8090),
|
EXCALIDRAW_PORT: zod_1.z.coerce.number().default(8090),
|
||||||
EXCALIDRAW_EMBED_PORT: zod_1.z.coerce.number().default(8886),
|
EXCALIDRAW_EMBED_PORT: zod_1.z.coerce.number().default(8886),
|
||||||
|
// Homepage (service dashboard)
|
||||||
|
HOMEPAGE_URL: zod_1.z.string().default('http://homepage-changemaker:3000'),
|
||||||
|
HOMEPAGE_EMBED_PORT: zod_1.z.coerce.number().default(8887),
|
||||||
|
// Vaultwarden (password manager)
|
||||||
|
VAULTWARDEN_URL: zod_1.z.string().default('http://vaultwarden-changemaker:80'),
|
||||||
|
VAULTWARDEN_ADMIN_TOKEN: zod_1.z.string().default(''),
|
||||||
|
VAULTWARDEN_EMBED_PORT: zod_1.z.coerce.number().default(8890),
|
||||||
|
// Rocket.Chat (team chat)
|
||||||
|
ROCKETCHAT_URL: zod_1.z.string().default('http://rocketchat-changemaker:3000'),
|
||||||
|
ROCKETCHAT_ADMIN_USER: zod_1.z.string().default(''),
|
||||||
|
ROCKETCHAT_ADMIN_PASSWORD: zod_1.z.string().default(''),
|
||||||
|
ROCKETCHAT_EMBED_PORT: zod_1.z.coerce.number().default(8891),
|
||||||
|
ENABLE_CHAT: zod_1.z.string().default('false'),
|
||||||
|
// Gancio (event management)
|
||||||
|
GANCIO_URL: zod_1.z.string().default('http://gancio-changemaker:13120'),
|
||||||
|
GANCIO_PORT: zod_1.z.coerce.number().default(8092),
|
||||||
|
GANCIO_EMBED_PORT: zod_1.z.coerce.number().default(8892),
|
||||||
|
GANCIO_ADMIN_USER: zod_1.z.string().default('admin'),
|
||||||
|
GANCIO_ADMIN_PASSWORD: zod_1.z.string().default(''),
|
||||||
|
GANCIO_SYNC_ENABLED: zod_1.z.string().default('false'),
|
||||||
|
// Jitsi Meet (video conferencing)
|
||||||
|
ENABLE_MEET: zod_1.z.string().default('false'),
|
||||||
|
JITSI_APP_ID: zod_1.z.string().default('changemaker'),
|
||||||
|
JITSI_APP_SECRET: zod_1.z.string().default(''),
|
||||||
|
JITSI_URL: zod_1.z.string().default('http://jitsi-web-changemaker:80'),
|
||||||
|
JITSI_EMBED_PORT: zod_1.z.coerce.number().default(8893),
|
||||||
// Pangolin (tunnel / reverse proxy)
|
// Pangolin (tunnel / reverse proxy)
|
||||||
PANGOLIN_API_URL: zod_1.z.string()
|
PANGOLIN_API_URL: zod_1.z.string()
|
||||||
.default('')
|
.default('')
|
||||||
@ -96,6 +129,12 @@ const envSchema = zod_1.z.object({
|
|||||||
PANGOLIN_NEWT_SECRET: zod_1.z.string().default(''),
|
PANGOLIN_NEWT_SECRET: zod_1.z.string().default(''),
|
||||||
// NAR (National Address Register)
|
// NAR (National Address Register)
|
||||||
NAR_DATA_DIR: zod_1.z.string().default('/data'),
|
NAR_DATA_DIR: zod_1.z.string().default('/data'),
|
||||||
|
// Overpass / Area Import
|
||||||
|
OVERPASS_API_URL: zod_1.z.string().default('https://overpass-api.de/api/interpreter'),
|
||||||
|
OVERPASS_MIN_DELAY_MS: zod_1.z.coerce.number().default(30000),
|
||||||
|
AREA_IMPORT_MAX_GRID_POINTS: zod_1.z.coerce.number().default(500),
|
||||||
|
// Payments (Stripe)
|
||||||
|
ENABLE_PAYMENTS: zod_1.z.string().default('false'),
|
||||||
// Media Management
|
// Media Management
|
||||||
ENABLE_MEDIA_FEATURES: zod_1.z.string().default('false'),
|
ENABLE_MEDIA_FEATURES: zod_1.z.string().default('false'),
|
||||||
MEDIA_API_PORT: zod_1.z.coerce.number().default(4100),
|
MEDIA_API_PORT: zod_1.z.coerce.number().default(4100),
|
||||||
@ -103,7 +142,25 @@ const envSchema = zod_1.z.object({
|
|||||||
MEDIA_ROOT: zod_1.z.string().default('/media/library'),
|
MEDIA_ROOT: zod_1.z.string().default('/media/library'),
|
||||||
MEDIA_UPLOADS: zod_1.z.string().default('/media/uploads'),
|
MEDIA_UPLOADS: zod_1.z.string().default('/media/uploads'),
|
||||||
MAX_UPLOAD_SIZE_GB: zod_1.z.coerce.number().default(10),
|
MAX_UPLOAD_SIZE_GB: zod_1.z.coerce.number().default(10),
|
||||||
PUBLIC_MEDIA_PORT: zod_1.z.coerce.number().default(3100),
|
// Container Registry
|
||||||
|
GITEA_REGISTRY: zod_1.z.string().default('gitea.bnkops.com/admin'),
|
||||||
|
GITEA_REGISTRY_USER: zod_1.z.string().default(''),
|
||||||
|
GITEA_REGISTRY_PASS: zod_1.z.string().default(''),
|
||||||
|
// Gitea Docs Comments
|
||||||
|
GITEA_COMMENTS_ENABLED: zod_1.z.string().default('false'),
|
||||||
|
GITEA_API_TOKEN: zod_1.z.string().default(''),
|
||||||
|
GITEA_COMMENTS_REPO_OWNER: zod_1.z.string().default(''),
|
||||||
|
GITEA_COMMENTS_REPO_NAME: zod_1.z.string().default('docs-comments'),
|
||||||
|
GITEA_OAUTH_CLIENT_ID: zod_1.z.string().default(''),
|
||||||
|
GITEA_OAUTH_CLIENT_SECRET: zod_1.z.string().default(''),
|
||||||
|
// SMS Campaigns (Termux Android bridge)
|
||||||
|
ENABLE_SMS: zod_1.z.string().default('false'),
|
||||||
|
TERMUX_API_URL: zod_1.z.string().default('http://10.0.0.193:5001'),
|
||||||
|
TERMUX_API_KEY: zod_1.z.string().default(''),
|
||||||
|
SMS_DELAY_BETWEEN_MS: zod_1.z.coerce.number().default(3000),
|
||||||
|
SMS_MAX_RETRIES: zod_1.z.coerce.number().default(3),
|
||||||
|
SMS_RESPONSE_SYNC_INTERVAL_MS: zod_1.z.coerce.number().default(30000),
|
||||||
|
SMS_DEVICE_MONITOR_INTERVAL_MS: zod_1.z.coerce.number().default(30000),
|
||||||
// Docs / Code Server
|
// Docs / Code Server
|
||||||
CODE_SERVER_URL: zod_1.z.string().default('http://code-server-changemaker:8080'),
|
CODE_SERVER_URL: zod_1.z.string().default('http://code-server-changemaker:8080'),
|
||||||
CODE_SERVER_PORT: zod_1.z.coerce.number().default(8888),
|
CODE_SERVER_PORT: zod_1.z.coerce.number().default(8888),
|
||||||
@ -119,8 +176,10 @@ const envSchema = zod_1.z.object({
|
|||||||
PROMETHEUS_PORT: zod_1.z.coerce.number().default(9090),
|
PROMETHEUS_PORT: zod_1.z.coerce.number().default(9090),
|
||||||
GRAFANA_URL: zod_1.z.string().default('http://grafana-changemaker:3000'),
|
GRAFANA_URL: zod_1.z.string().default('http://grafana-changemaker:3000'),
|
||||||
GRAFANA_PORT: zod_1.z.coerce.number().default(3005),
|
GRAFANA_PORT: zod_1.z.coerce.number().default(3005),
|
||||||
|
GRAFANA_EMBED_PORT: zod_1.z.coerce.number().default(8894),
|
||||||
ALERTMANAGER_URL: zod_1.z.string().default('http://alertmanager-changemaker:9093'),
|
ALERTMANAGER_URL: zod_1.z.string().default('http://alertmanager-changemaker:9093'),
|
||||||
ALERTMANAGER_PORT: zod_1.z.coerce.number().default(9093),
|
ALERTMANAGER_PORT: zod_1.z.coerce.number().default(9093),
|
||||||
|
ALERTMANAGER_EMBED_PORT: zod_1.z.coerce.number().default(8895),
|
||||||
CADVISOR_URL: zod_1.z.string().default('http://cadvisor-changemaker:8080'),
|
CADVISOR_URL: zod_1.z.string().default('http://cadvisor-changemaker:8080'),
|
||||||
CADVISOR_PORT: zod_1.z.coerce.number().default(8086),
|
CADVISOR_PORT: zod_1.z.coerce.number().default(8086),
|
||||||
NODE_EXPORTER_URL: zod_1.z.string().default('http://node-exporter-changemaker:9100'),
|
NODE_EXPORTER_URL: zod_1.z.string().default('http://node-exporter-changemaker:9100'),
|
||||||
|
|||||||
2
api/dist/config/env.js.map
vendored
2
api/dist/config/env.js.map
vendored
File diff suppressed because one or more lines are too long
67
api/dist/media-server.js
vendored
67
api/dist/media-server.js
vendored
@ -11,13 +11,30 @@ const logger_1 = require("./utils/logger");
|
|||||||
const videos_routes_1 = require("./modules/media/routes/videos.routes");
|
const videos_routes_1 = require("./modules/media/routes/videos.routes");
|
||||||
const video_streaming_routes_1 = require("./modules/media/routes/video-streaming.routes");
|
const video_streaming_routes_1 = require("./modules/media/routes/video-streaming.routes");
|
||||||
const reactions_routes_1 = require("./modules/media/routes/reactions.routes");
|
const reactions_routes_1 = require("./modules/media/routes/reactions.routes");
|
||||||
const public_media_routes_1 = require("./modules/media/routes/public-media.routes");
|
const public_routes_1 = require("./modules/media/routes/public.routes");
|
||||||
|
const chat_stream_routes_1 = require("./modules/media/routes/chat-stream.routes");
|
||||||
const comments_routes_1 = require("./modules/media/routes/comments.routes");
|
const comments_routes_1 = require("./modules/media/routes/comments.routes");
|
||||||
const upload_routes_1 = require("./modules/media/routes/upload.routes");
|
const upload_routes_1 = require("./modules/media/routes/upload.routes");
|
||||||
const video_actions_routes_1 = require("./modules/media/routes/video-actions.routes");
|
const video_actions_routes_1 = require("./modules/media/routes/video-actions.routes");
|
||||||
const video_schedule_routes_1 = require("./modules/media/routes/video-schedule.routes");
|
const video_schedule_routes_1 = require("./modules/media/routes/video-schedule.routes");
|
||||||
const video_tracking_routes_1 = require("./modules/media/routes/video-tracking.routes");
|
const video_tracking_routes_1 = require("./modules/media/routes/video-tracking.routes");
|
||||||
|
const comment_admin_routes_1 = require("./modules/media/routes/comment-admin.routes");
|
||||||
|
const chat_notifications_routes_1 = require("./modules/media/routes/chat-notifications.routes");
|
||||||
|
const chat_threads_routes_1 = require("./modules/media/routes/chat-threads.routes");
|
||||||
|
const user_profile_routes_1 = require("./modules/media/routes/user-profile.routes");
|
||||||
|
const shorts_routes_1 = require("./modules/media/routes/shorts.routes");
|
||||||
|
const upvote_routes_1 = require("./modules/media/routes/upvote.routes");
|
||||||
const video_schedule_queue_service_1 = require("./services/video-schedule-queue.service");
|
const video_schedule_queue_service_1 = require("./services/video-schedule-queue.service");
|
||||||
|
const video_fetch_queue_service_1 = require("./services/video-fetch-queue.service");
|
||||||
|
const fetch_routes_1 = require("./modules/media/routes/fetch.routes");
|
||||||
|
const playlists_public_routes_1 = require("./modules/media/routes/playlists-public.routes");
|
||||||
|
const playlists_user_routes_1 = require("./modules/media/routes/playlists-user.routes");
|
||||||
|
const playlists_admin_routes_1 = require("./modules/media/routes/playlists-admin.routes");
|
||||||
|
const photos_routes_1 = require("./modules/media/routes/photos.routes");
|
||||||
|
const photo_upload_routes_1 = require("./modules/media/routes/photo-upload.routes");
|
||||||
|
const photo_albums_routes_1 = require("./modules/media/routes/photo-albums.routes");
|
||||||
|
const photos_public_routes_1 = require("./modules/media/routes/photos-public.routes");
|
||||||
|
const photo_engagement_routes_1 = require("./modules/media/routes/photo-engagement.routes");
|
||||||
// Add BigInt serialization support for Prisma BigInt fields
|
// Add BigInt serialization support for Prisma BigInt fields
|
||||||
// This converts BigInt values to strings when JSON.stringify() is called
|
// This converts BigInt values to strings when JSON.stringify() is called
|
||||||
BigInt.prototype.toJSON = function () {
|
BigInt.prototype.toJSON = function () {
|
||||||
@ -34,6 +51,7 @@ const fastify = (0, fastify_1.default)({
|
|||||||
process.on('SIGTERM', async () => {
|
process.on('SIGTERM', async () => {
|
||||||
logger_1.logger.info('SIGTERM received, shutting down gracefully...');
|
logger_1.logger.info('SIGTERM received, shutting down gracefully...');
|
||||||
await video_schedule_queue_service_1.videoScheduleQueueService.close();
|
await video_schedule_queue_service_1.videoScheduleQueueService.close();
|
||||||
|
await video_fetch_queue_service_1.videoFetchQueueService.close();
|
||||||
fastify.close(() => {
|
fastify.close(() => {
|
||||||
logger_1.logger.info('Media API server closed');
|
logger_1.logger.info('Media API server closed');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
@ -52,8 +70,23 @@ process.on('uncaughtException', (error) => {
|
|||||||
// Start server
|
// Start server
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
try {
|
try {
|
||||||
// CORS configuration
|
// CORS configuration — allow admin app + MkDocs docs site
|
||||||
const allowedOrigins = env_1.env.CORS_ORIGINS.split(',').map(o => o.trim());
|
const allowedOrigins = env_1.env.CORS_ORIGINS.split(',').map(o => o.trim());
|
||||||
|
// Auto-add MkDocs origins so video cards/players work in docs
|
||||||
|
const mkdocsOrigin = `http://localhost:${env_1.env.MKDOCS_PORT || 4003}`;
|
||||||
|
if (!allowedOrigins.includes(mkdocsOrigin)) {
|
||||||
|
allowedOrigins.push(mkdocsOrigin);
|
||||||
|
}
|
||||||
|
// Also allow the docs subdomain in production (docs.domain.org)
|
||||||
|
for (const origin of [...allowedOrigins]) {
|
||||||
|
const match = origin.match(/^(https?:\/\/)app\./);
|
||||||
|
if (match) {
|
||||||
|
const docsOrigin = origin.replace(/^(https?:\/\/)app\./, '$1docs.');
|
||||||
|
if (!allowedOrigins.includes(docsOrigin)) {
|
||||||
|
allowedOrigins.push(docsOrigin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
await fastify.register(cors_1.default, {
|
await fastify.register(cors_1.default, {
|
||||||
origin: (origin, cb) => {
|
origin: (origin, cb) => {
|
||||||
// Allow requests with no origin (mobile apps, curl, etc.)
|
// Allow requests with no origin (mobile apps, curl, etc.)
|
||||||
@ -93,10 +126,29 @@ const start = async () => {
|
|||||||
await fastify.register(video_schedule_routes_1.videoScheduleRoutes, { prefix: '/api/videos' });
|
await fastify.register(video_schedule_routes_1.videoScheduleRoutes, { prefix: '/api/videos' });
|
||||||
await fastify.register(video_tracking_routes_1.videoTrackingRoutes, { prefix: '/api/track' });
|
await fastify.register(video_tracking_routes_1.videoTrackingRoutes, { prefix: '/api/track' });
|
||||||
await fastify.register(reactions_routes_1.reactionsRoutes, { prefix: '/api/reactions' });
|
await fastify.register(reactions_routes_1.reactionsRoutes, { prefix: '/api/reactions' });
|
||||||
await fastify.register(public_media_routes_1.publicMediaRoutes, { prefix: '/api/media' });
|
await fastify.register(public_routes_1.publicRoutes, { prefix: '/api' });
|
||||||
await fastify.register(comments_routes_1.commentsRoutes, { prefix: '/api/media' });
|
await fastify.register(comments_routes_1.commentsRoutes, { prefix: '/api' });
|
||||||
// TODO: Add more routes
|
await fastify.register(chat_stream_routes_1.chatStreamRoutes, { prefix: '/api' });
|
||||||
// await fastify.register(jobsRoutes, { prefix: '/api/jobs' });
|
await fastify.register(comment_admin_routes_1.commentAdminRoutes, { prefix: '/api/media' });
|
||||||
|
await fastify.register(chat_notifications_routes_1.chatNotificationsRoutes, { prefix: '/api/media' });
|
||||||
|
await fastify.register(chat_threads_routes_1.chatThreadsRoutes, { prefix: '/api/media' });
|
||||||
|
await fastify.register(user_profile_routes_1.userProfileRoutes, { prefix: '/api/media' });
|
||||||
|
await fastify.register(fetch_routes_1.fetchRoutes, { prefix: '/api/videos' });
|
||||||
|
await fastify.register(shorts_routes_1.shortsRoutes, { prefix: '/api' });
|
||||||
|
await fastify.register(upvote_routes_1.upvoteRoutes, { prefix: '/api' });
|
||||||
|
await fastify.register(playlists_public_routes_1.playlistsPublicRoutes, { prefix: '/api/playlists' });
|
||||||
|
await fastify.register(playlists_user_routes_1.playlistsUserRoutes, { prefix: '/api/playlists' });
|
||||||
|
await fastify.register(playlists_admin_routes_1.playlistsAdminRoutes, { prefix: '/api/media' });
|
||||||
|
// Photo gallery routes
|
||||||
|
await fastify.register(photos_routes_1.photosRoutes, { prefix: '/api/photos' });
|
||||||
|
await fastify.register(photo_upload_routes_1.photoUploadRoutes, { prefix: '/api/photos' });
|
||||||
|
await fastify.register(photo_albums_routes_1.photoAlbumsRoutes, { prefix: '/api/albums' });
|
||||||
|
await fastify.register(photos_public_routes_1.photosPublicRoutes, { prefix: '/api' });
|
||||||
|
await fastify.register(photo_engagement_routes_1.photoEngagementRoutes, { prefix: '/api' });
|
||||||
|
// 404 handler for unmatched routes
|
||||||
|
fastify.setNotFoundHandler((_request, reply) => {
|
||||||
|
reply.status(404).send({ error: { message: 'Route not found', code: 'NOT_FOUND' } });
|
||||||
|
});
|
||||||
const port = env_1.env.MEDIA_API_PORT;
|
const port = env_1.env.MEDIA_API_PORT;
|
||||||
const host = '0.0.0.0';
|
const host = '0.0.0.0';
|
||||||
await fastify.listen({ port, host });
|
await fastify.listen({ port, host });
|
||||||
@ -104,6 +156,9 @@ const start = async () => {
|
|||||||
// Start video schedule queue worker
|
// Start video schedule queue worker
|
||||||
video_schedule_queue_service_1.videoScheduleQueueService.startWorker();
|
video_schedule_queue_service_1.videoScheduleQueueService.startWorker();
|
||||||
logger_1.logger.info('Video schedule queue worker initialized');
|
logger_1.logger.info('Video schedule queue worker initialized');
|
||||||
|
// Start video fetch queue worker
|
||||||
|
video_fetch_queue_service_1.videoFetchQueueService.startWorker();
|
||||||
|
logger_1.logger.info('Video fetch queue worker initialized');
|
||||||
if (env_1.env.ENABLE_MEDIA_FEATURES !== 'true') {
|
if (env_1.env.ENABLE_MEDIA_FEATURES !== 'true') {
|
||||||
logger_1.logger.warn('Media features are disabled (ENABLE_MEDIA_FEATURES=false)');
|
logger_1.logger.warn('Media features are disabled (ENABLE_MEDIA_FEATURES=false)');
|
||||||
}
|
}
|
||||||
|
|||||||
2
api/dist/media-server.js.map
vendored
2
api/dist/media-server.js.map
vendored
File diff suppressed because one or more lines are too long
2
api/dist/middleware/auth.middleware.d.ts.map
vendored
2
api/dist/middleware/auth.middleware.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"auth.middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAY1D,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,QAe5E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,QAiB5E"}
|
{"version":3,"file":"auth.middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAa1D,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,QAoB5E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,QAsB5E"}
|
||||||
18
api/dist/middleware/auth.middleware.js
vendored
18
api/dist/middleware/auth.middleware.js
vendored
@ -15,8 +15,13 @@ function authenticate(req, _res, next) {
|
|||||||
}
|
}
|
||||||
const token = header.slice(7);
|
const token = header.slice(7);
|
||||||
try {
|
try {
|
||||||
const payload = jsonwebtoken_1.default.verify(token, env_1.env.JWT_ACCESS_SECRET);
|
const payload = jsonwebtoken_1.default.verify(token, env_1.env.JWT_ACCESS_SECRET, { algorithms: ['HS256'] });
|
||||||
req.user = { id: payload.id, email: payload.email, role: payload.role };
|
req.user = {
|
||||||
|
id: payload.id,
|
||||||
|
email: payload.email,
|
||||||
|
role: payload.role,
|
||||||
|
roles: payload.roles || [payload.role], // Backwards compat: old JWTs without roles
|
||||||
|
};
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
@ -31,8 +36,13 @@ function optionalAuth(req, _res, next) {
|
|||||||
}
|
}
|
||||||
const token = header.slice(7);
|
const token = header.slice(7);
|
||||||
try {
|
try {
|
||||||
const payload = jsonwebtoken_1.default.verify(token, env_1.env.JWT_ACCESS_SECRET);
|
const payload = jsonwebtoken_1.default.verify(token, env_1.env.JWT_ACCESS_SECRET, { algorithms: ['HS256'] });
|
||||||
req.user = { id: payload.id, email: payload.email, role: payload.role };
|
req.user = {
|
||||||
|
id: payload.id,
|
||||||
|
email: payload.email,
|
||||||
|
role: payload.role,
|
||||||
|
roles: payload.roles || [payload.role],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
// Token invalid — continue without user
|
// Token invalid — continue without user
|
||||||
|
|||||||
2
api/dist/middleware/auth.middleware.js.map
vendored
2
api/dist/middleware/auth.middleware.js.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"auth.middleware.js","sourceRoot":"","sources":["../../src/middleware/auth.middleware.ts"],"names":[],"mappings":";;;;;AAYA,oCAeC;AAED,oCAiBC;AA7CD,gEAA+B;AAE/B,uCAAoC;AACpC,mDAA2C;AAQ3C,SAAgB,YAAY,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IAC3E,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,yBAAyB,EAAE,eAAe,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE9B,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,SAAG,CAAC,iBAAiB,CAAiB,CAAC;QACzE,GAAG,CAAC,IAAI,GAAG,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;QACxE,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,0BAA0B,EAAE,eAAe,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED,SAAgB,YAAY,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IAC3E,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACnC,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE9B,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,SAAG,CAAC,iBAAiB,CAAiB,CAAC;QACzE,GAAG,CAAC,IAAI,GAAG,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;IAC1E,CAAC;IAAC,MAAM,CAAC;QACP,wCAAwC;IAC1C,CAAC;IAED,IAAI,EAAE,CAAC;AACT,CAAC"}
|
{"version":3,"file":"auth.middleware.js","sourceRoot":"","sources":["../../src/middleware/auth.middleware.ts"],"names":[],"mappings":";;;;;AAaA,oCAoBC;AAED,oCAsBC;AAxDD,gEAA+B;AAE/B,uCAAoC;AACpC,mDAA2C;AAS3C,SAAgB,YAAY,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IAC3E,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,yBAAyB,EAAE,eAAe,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE9B,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,SAAG,CAAC,iBAAiB,EAAE,EAAE,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAAiB,CAAC;QACpG,GAAG,CAAC,IAAI,GAAG;YACT,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,2CAA2C;SACpF,CAAC;QACF,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,0BAA0B,EAAE,eAAe,CAAC,CAAC;IACvE,CAAC;AACH,CAAC;AAED,SAAgB,YAAY,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IAC3E,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACnC,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE9B,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,SAAG,CAAC,iBAAiB,EAAE,EAAE,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAAiB,CAAC;QACpG,GAAG,CAAC,IAAI,GAAG;YACT,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;SACvC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,wCAAwC;IAC1C,CAAC;IAED,IAAI,EAAE,CAAC;AACT,CAAC"}
|
||||||
12
api/dist/middleware/rate-limit.d.ts
vendored
12
api/dist/middleware/rate-limit.d.ts
vendored
@ -6,7 +6,19 @@ export declare const canvassVisitRateLimit: import("express-rate-limit").RateLim
|
|||||||
export declare const canvassBulkVisitRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
export declare const canvassBulkVisitRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
export declare const gpsTrackingRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
export declare const gpsTrackingRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
export declare const canvassGeocodeRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
export declare const canvassGeocodeRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
|
export declare const adTrackingRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
|
export declare const quickJoinRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
export declare const authRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
export declare const authRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
export declare const observabilityRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
export declare const observabilityRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
|
export declare const docsAnalyticsRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
|
export declare const docsCommentAnonRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
|
export declare const docsCommentAuthRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
|
export declare const docsCommentFetchRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
|
export declare const profileViewRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
|
export declare const profileEditRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
|
export declare const profilePhotoRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
|
export declare const profilePasswordRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
|
export declare const eventSubmissionRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
|
export declare const errorReportRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
export declare const healthMetricsRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
export declare const healthMetricsRateLimit: import("express-rate-limit").RateLimitRequestHandler;
|
||||||
//# sourceMappingURL=rate-limit.d.ts.map
|
//# sourceMappingURL=rate-limit.d.ts.map
|
||||||
2
api/dist/middleware/rate-limit.d.ts.map
vendored
2
api/dist/middleware/rate-limit.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,eAAe,sDAe1B,CAAC;AAEH,eAAO,MAAM,cAAc,sDAezB,CAAC;AAEH,eAAO,MAAM,iBAAiB,sDAe5B,CAAC;AAEH,eAAO,MAAM,oBAAoB,sDAe/B,CAAC;AAEH,eAAO,MAAM,qBAAqB,sDAehC,CAAC;AAEH,eAAO,MAAM,yBAAyB,sDAepC,CAAC;AAEH,eAAO,MAAM,oBAAoB,sDAe/B,CAAC;AAEH,eAAO,MAAM,uBAAuB,sDAelC,CAAC;AAEH,eAAO,MAAM,aAAa,sDAexB,CAAC;AAEH,eAAO,MAAM,sBAAsB,sDAejC,CAAC;AAEH,eAAO,MAAM,sBAAsB,sDAejC,CAAC"}
|
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,eAAe,sDAe1B,CAAC;AAEH,eAAO,MAAM,cAAc,sDAezB,CAAC;AAEH,eAAO,MAAM,iBAAiB,sDAe5B,CAAC;AAEH,eAAO,MAAM,oBAAoB,sDAe/B,CAAC;AAEH,eAAO,MAAM,qBAAqB,sDAehC,CAAC;AAEH,eAAO,MAAM,yBAAyB,sDAepC,CAAC;AAEH,eAAO,MAAM,oBAAoB,sDAe/B,CAAC;AAEH,eAAO,MAAM,uBAAuB,sDAelC,CAAC;AAEH,eAAO,MAAM,mBAAmB,sDAe9B,CAAC;AAEH,eAAO,MAAM,kBAAkB,sDAe7B,CAAC;AAEH,eAAO,MAAM,aAAa,sDAexB,CAAC;AAEH,eAAO,MAAM,sBAAsB,sDAejC,CAAC;AAEH,eAAO,MAAM,sBAAsB,sDAejC,CAAC;AAEH,eAAO,MAAM,wBAAwB,sDAenC,CAAC;AAEH,eAAO,MAAM,wBAAwB,sDAenC,CAAC;AAEH,eAAO,MAAM,yBAAyB,sDAepC,CAAC;AAEH,eAAO,MAAM,oBAAoB,sDAe/B,CAAC;AAEH,eAAO,MAAM,oBAAoB,sDAe/B,CAAC;AAEH,eAAO,MAAM,qBAAqB,sDAehC,CAAC;AAEH,eAAO,MAAM,wBAAwB,sDAenC,CAAC;AAEH,eAAO,MAAM,wBAAwB,sDAenC,CAAC;AAEH,eAAO,MAAM,oBAAoB,sDAe/B,CAAC;AAEH,eAAO,MAAM,sBAAsB,sDAejC,CAAC"}
|
||||||
194
api/dist/middleware/rate-limit.js
vendored
194
api/dist/middleware/rate-limit.js
vendored
@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.healthMetricsRateLimit = exports.observabilityRateLimit = exports.authRateLimit = exports.canvassGeocodeRateLimit = exports.gpsTrackingRateLimit = exports.canvassBulkVisitRateLimit = exports.canvassVisitRateLimit = exports.shiftSignupRateLimit = exports.responseRateLimit = exports.emailRateLimit = exports.globalRateLimit = void 0;
|
exports.healthMetricsRateLimit = exports.errorReportRateLimit = exports.eventSubmissionRateLimit = exports.profilePasswordRateLimit = exports.profilePhotoRateLimit = exports.profileEditRateLimit = exports.profileViewRateLimit = exports.docsCommentFetchRateLimit = exports.docsCommentAuthRateLimit = exports.docsCommentAnonRateLimit = exports.docsAnalyticsRateLimit = exports.observabilityRateLimit = exports.authRateLimit = exports.quickJoinRateLimit = exports.adTrackingRateLimit = exports.canvassGeocodeRateLimit = exports.gpsTrackingRateLimit = exports.canvassBulkVisitRateLimit = exports.canvassVisitRateLimit = exports.shiftSignupRateLimit = exports.responseRateLimit = exports.emailRateLimit = exports.globalRateLimit = void 0;
|
||||||
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
||||||
const rate_limit_redis_1 = __importDefault(require("rate-limit-redis"));
|
const rate_limit_redis_1 = __importDefault(require("rate-limit-redis"));
|
||||||
const redis_1 = require("../config/redis");
|
const redis_1 = require("../config/redis");
|
||||||
@ -136,6 +136,38 @@ exports.canvassGeocodeRateLimit = (0, express_rate_limit_1.default)({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
exports.adTrackingRateLimit = (0, express_rate_limit_1.default)({
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
max: 60, // 60 events/min per IP (generous for scroll-heavy gallery pages)
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
store: new rate_limit_redis_1.default({
|
||||||
|
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||||
|
prefix: 'rl:ad-track:',
|
||||||
|
}),
|
||||||
|
message: {
|
||||||
|
error: {
|
||||||
|
message: 'Too many tracking requests',
|
||||||
|
code: 'RATE_LIMIT_EXCEEDED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
exports.quickJoinRateLimit = (0, express_rate_limit_1.default)({
|
||||||
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
|
max: 10,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
store: new rate_limit_redis_1.default({
|
||||||
|
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||||
|
prefix: 'rl:quick-join:',
|
||||||
|
}),
|
||||||
|
message: {
|
||||||
|
error: {
|
||||||
|
message: 'Too many join attempts, please try again later',
|
||||||
|
code: 'QUICK_JOIN_RATE_LIMIT_EXCEEDED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
exports.authRateLimit = (0, express_rate_limit_1.default)({
|
exports.authRateLimit = (0, express_rate_limit_1.default)({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 10, // Reduced from 20 to prevent brute force attacks
|
max: 10, // Reduced from 20 to prevent brute force attacks
|
||||||
@ -168,6 +200,166 @@ exports.observabilityRateLimit = (0, express_rate_limit_1.default)({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
exports.docsAnalyticsRateLimit = (0, express_rate_limit_1.default)({
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
max: 60, // 60 requests/min per IP
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
store: new rate_limit_redis_1.default({
|
||||||
|
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||||
|
prefix: 'rl:docs-analytics:',
|
||||||
|
}),
|
||||||
|
message: {
|
||||||
|
error: {
|
||||||
|
message: 'Too many tracking requests, please slow down',
|
||||||
|
code: 'DOCS_ANALYTICS_RATE_LIMIT_EXCEEDED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
exports.docsCommentAnonRateLimit = (0, express_rate_limit_1.default)({
|
||||||
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
|
max: 5,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
store: new rate_limit_redis_1.default({
|
||||||
|
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||||
|
prefix: 'rl:docs-comment-anon:',
|
||||||
|
}),
|
||||||
|
message: {
|
||||||
|
error: {
|
||||||
|
message: 'Too many anonymous comments, please try again later',
|
||||||
|
code: 'DOCS_COMMENT_ANON_RATE_LIMIT_EXCEEDED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
exports.docsCommentAuthRateLimit = (0, express_rate_limit_1.default)({
|
||||||
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
|
max: 30,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
store: new rate_limit_redis_1.default({
|
||||||
|
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||||
|
prefix: 'rl:docs-comment-auth:',
|
||||||
|
}),
|
||||||
|
message: {
|
||||||
|
error: {
|
||||||
|
message: 'Too many comments, please try again later',
|
||||||
|
code: 'DOCS_COMMENT_AUTH_RATE_LIMIT_EXCEEDED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
exports.docsCommentFetchRateLimit = (0, express_rate_limit_1.default)({
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
max: 60,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
store: new rate_limit_redis_1.default({
|
||||||
|
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||||
|
prefix: 'rl:docs-comment-fetch:',
|
||||||
|
}),
|
||||||
|
message: {
|
||||||
|
error: {
|
||||||
|
message: 'Too many comment fetch requests, please slow down',
|
||||||
|
code: 'DOCS_COMMENT_FETCH_RATE_LIMIT_EXCEEDED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
exports.profileViewRateLimit = (0, express_rate_limit_1.default)({
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
max: 60, // 60 requests per minute (shared across profile, photo, activity endpoints)
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
store: new rate_limit_redis_1.default({
|
||||||
|
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||||
|
prefix: 'rl:profile-view:',
|
||||||
|
}),
|
||||||
|
message: {
|
||||||
|
error: {
|
||||||
|
message: 'Too many profile requests, please try again later',
|
||||||
|
code: 'PROFILE_VIEW_RATE_LIMIT_EXCEEDED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
exports.profileEditRateLimit = (0, express_rate_limit_1.default)({
|
||||||
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
|
max: 20,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
store: new rate_limit_redis_1.default({
|
||||||
|
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||||
|
prefix: 'rl:profile-edit:',
|
||||||
|
}),
|
||||||
|
message: {
|
||||||
|
error: {
|
||||||
|
message: 'Too many profile edit requests, please try again later',
|
||||||
|
code: 'PROFILE_EDIT_RATE_LIMIT_EXCEEDED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
exports.profilePhotoRateLimit = (0, express_rate_limit_1.default)({
|
||||||
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
|
max: 5,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
store: new rate_limit_redis_1.default({
|
||||||
|
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||||
|
prefix: 'rl:profile-photo:',
|
||||||
|
}),
|
||||||
|
message: {
|
||||||
|
error: {
|
||||||
|
message: 'Too many photo uploads, please try again later',
|
||||||
|
code: 'PROFILE_PHOTO_RATE_LIMIT_EXCEEDED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
exports.profilePasswordRateLimit = (0, express_rate_limit_1.default)({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 5, // Stricter than auth (10/15min) — profile passwords may be simpler
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
store: new rate_limit_redis_1.default({
|
||||||
|
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||||
|
prefix: 'rl:profile-password:',
|
||||||
|
}),
|
||||||
|
message: {
|
||||||
|
error: {
|
||||||
|
message: 'Too many password attempts, please try again later',
|
||||||
|
code: 'PROFILE_PASSWORD_RATE_LIMIT_EXCEEDED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
exports.eventSubmissionRateLimit = (0, express_rate_limit_1.default)({
|
||||||
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
|
max: 5,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
store: new rate_limit_redis_1.default({
|
||||||
|
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||||
|
prefix: 'rl:event-submit:',
|
||||||
|
}),
|
||||||
|
message: {
|
||||||
|
error: {
|
||||||
|
message: 'Too many event submissions, please try again later',
|
||||||
|
code: 'EVENT_SUBMISSION_RATE_LIMIT_EXCEEDED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
exports.errorReportRateLimit = (0, express_rate_limit_1.default)({
|
||||||
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
|
max: 5,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
store: new rate_limit_redis_1.default({
|
||||||
|
sendCommand: (command, ...args) => redis_1.redis.call(command, ...args),
|
||||||
|
prefix: 'rl:error-report:',
|
||||||
|
}),
|
||||||
|
message: {
|
||||||
|
error: {
|
||||||
|
message: 'Too many error reports, please try again later',
|
||||||
|
code: 'ERROR_REPORT_RATE_LIMIT_EXCEEDED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
exports.healthMetricsRateLimit = (0, express_rate_limit_1.default)({
|
exports.healthMetricsRateLimit = (0, express_rate_limit_1.default)({
|
||||||
windowMs: 60 * 1000, // 1 minute
|
windowMs: 60 * 1000, // 1 minute
|
||||||
max: 30, // 30 requests per minute
|
max: 30, // 30 requests per minute
|
||||||
|
|||||||
2
api/dist/middleware/rate-limit.js.map
vendored
2
api/dist/middleware/rate-limit.js.map
vendored
File diff suppressed because one or more lines are too long
2
api/dist/middleware/rbac.middleware.d.ts.map
vendored
2
api/dist/middleware/rbac.middleware.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"rbac.middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/rbac.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG1C,wBAAgB,WAAW,CAAC,GAAG,KAAK,EAAE,QAAQ,EAAE,IACtC,KAAK,OAAO,EAAE,MAAM,QAAQ,EAAE,MAAM,YAAY,UAWzD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,QAU9E"}
|
{"version":3,"file":"rbac.middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/rbac.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG1C,wBAAgB,WAAW,CAAC,GAAG,KAAK,EAAE,QAAQ,EAAE,IACtC,KAAK,OAAO,EAAE,MAAM,QAAQ,EAAE,MAAM,YAAY,UAqBzD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,QAY9E"}
|
||||||
13
api/dist/middleware/rbac.middleware.js
vendored
13
api/dist/middleware/rbac.middleware.js
vendored
@ -9,7 +9,14 @@ function requireRole(...roles) {
|
|||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
throw new error_handler_1.AppError(401, 'Authentication required', 'AUTH_REQUIRED');
|
throw new error_handler_1.AppError(401, 'Authentication required', 'AUTH_REQUIRED');
|
||||||
}
|
}
|
||||||
if (!roles.includes(req.user.role)) {
|
// Check multi-role array (falls back to single role via auth middleware)
|
||||||
|
const userRoles = req.user.roles || [req.user.role];
|
||||||
|
// SUPER_ADMIN bypasses all role checks
|
||||||
|
if (userRoles.includes(client_1.UserRole.SUPER_ADMIN)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
const hasRole = userRoles.some(r => roles.includes(r));
|
||||||
|
if (!hasRole) {
|
||||||
throw new error_handler_1.AppError(403, 'Insufficient permissions', 'FORBIDDEN');
|
throw new error_handler_1.AppError(403, 'Insufficient permissions', 'FORBIDDEN');
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
@ -19,7 +26,9 @@ function requireNonTemp(req, _res, next) {
|
|||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
throw new error_handler_1.AppError(401, 'Authentication required', 'AUTH_REQUIRED');
|
throw new error_handler_1.AppError(401, 'Authentication required', 'AUTH_REQUIRED');
|
||||||
}
|
}
|
||||||
if (req.user.role === client_1.UserRole.TEMP) {
|
const userRoles = req.user.roles || [req.user.role];
|
||||||
|
// User is "temp only" if their only role is TEMP
|
||||||
|
if (userRoles.length === 1 && userRoles[0] === client_1.UserRole.TEMP) {
|
||||||
throw new error_handler_1.AppError(403, 'Temporary accounts cannot access this resource', 'TEMP_FORBIDDEN');
|
throw new error_handler_1.AppError(403, 'Temporary accounts cannot access this resource', 'TEMP_FORBIDDEN');
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
|
|||||||
2
api/dist/middleware/rbac.middleware.js.map
vendored
2
api/dist/middleware/rbac.middleware.js.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"rbac.middleware.js","sourceRoot":"","sources":["../../src/middleware/rbac.middleware.ts"],"names":[],"mappings":";;AAIA,kCAYC;AAED,wCAUC;AA3BD,2CAA0C;AAC1C,mDAA2C;AAE3C,SAAgB,WAAW,CAAC,GAAG,KAAiB;IAC9C,OAAO,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB,EAAE,EAAE;QAC1D,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YACd,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,yBAAyB,EAAE,eAAe,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,0BAA0B,EAAE,WAAW,CAAC,CAAC;QACnE,CAAC;QAED,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC;AAED,SAAgB,cAAc,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IAC7E,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACd,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,yBAAyB,EAAE,eAAe,CAAC,CAAC;IACtE,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,iBAAQ,CAAC,IAAI,EAAE,CAAC;QACpC,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,gDAAgD,EAAE,gBAAgB,CAAC,CAAC;IAC9F,CAAC;IAED,IAAI,EAAE,CAAC;AACT,CAAC"}
|
{"version":3,"file":"rbac.middleware.js","sourceRoot":"","sources":["../../src/middleware/rbac.middleware.ts"],"names":[],"mappings":";;AAIA,kCAsBC;AAED,wCAYC;AAvCD,2CAA0C;AAC1C,mDAA2C;AAE3C,SAAgB,WAAW,CAAC,GAAG,KAAiB;IAC9C,OAAO,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB,EAAE,EAAE;QAC1D,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YACd,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,yBAAyB,EAAE,eAAe,CAAC,CAAC;QACtE,CAAC;QAED,yEAAyE;QACzE,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEpD,uCAAuC;QACvC,IAAI,SAAS,CAAC,QAAQ,CAAC,iBAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;YAC7C,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QAED,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAEvD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,0BAA0B,EAAE,WAAW,CAAC,CAAC;QACnE,CAAC;QAED,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC;AAED,SAAgB,cAAc,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IAC7E,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACd,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,yBAAyB,EAAE,eAAe,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpD,iDAAiD;IACjD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,KAAK,iBAAQ,CAAC,IAAI,EAAE,CAAC;QAC7D,MAAM,IAAI,wBAAQ,CAAC,GAAG,EAAE,gDAAgD,EAAE,gBAAgB,CAAC,CAAC;IAC9F,CAAC;IAED,IAAI,EAAE,CAAC;AACT,CAAC"}
|
||||||
2
api/dist/modules/auth/auth.routes.d.ts.map
vendored
2
api/dist/modules/auth/auth.routes.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"auth.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.routes.ts"],"names":[],"mappings":"AAOA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAmGxB,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,CAAC"}
|
{"version":3,"file":"auth.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.routes.ts"],"names":[],"mappings":"AAmBA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAmUxB,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,CAAC"}
|
||||||
233
api/dist/modules/auth/auth.routes.js
vendored
233
api/dist/modules/auth/auth.routes.js
vendored
@ -1,45 +1,27 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
if (k2 === undefined) k2 = k;
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
};
|
||||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
||||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
||||||
}
|
|
||||||
Object.defineProperty(o, k2, desc);
|
|
||||||
}) : (function(o, m, k, k2) {
|
|
||||||
if (k2 === undefined) k2 = k;
|
|
||||||
o[k2] = m[k];
|
|
||||||
}));
|
|
||||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
||||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
||||||
}) : function(o, v) {
|
|
||||||
o["default"] = v;
|
|
||||||
});
|
|
||||||
var __importStar = (this && this.__importStar) || (function () {
|
|
||||||
var ownKeys = function(o) {
|
|
||||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
||||||
var ar = [];
|
|
||||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
||||||
return ar;
|
|
||||||
};
|
|
||||||
return ownKeys(o);
|
|
||||||
};
|
|
||||||
return function (mod) {
|
|
||||||
if (mod && mod.__esModule) return mod;
|
|
||||||
var result = {};
|
|
||||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
||||||
__setModuleDefault(result, mod);
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.authRouter = void 0;
|
exports.authRouter = void 0;
|
||||||
const express_1 = require("express");
|
const express_1 = require("express");
|
||||||
|
const zod_1 = require("zod");
|
||||||
|
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
||||||
|
const client_1 = require("@prisma/client");
|
||||||
const auth_service_1 = require("./auth.service");
|
const auth_service_1 = require("./auth.service");
|
||||||
const auth_schemas_1 = require("./auth.schemas");
|
const auth_schemas_1 = require("./auth.schemas");
|
||||||
const validate_1 = require("../../middleware/validate");
|
const validate_1 = require("../../middleware/validate");
|
||||||
const auth_middleware_1 = require("../../middleware/auth.middleware");
|
const auth_middleware_1 = require("../../middleware/auth.middleware");
|
||||||
const rate_limit_1 = require("../../middleware/rate-limit");
|
const rate_limit_1 = require("../../middleware/rate-limit");
|
||||||
|
const database_1 = require("../../config/database");
|
||||||
|
const verification_token_service_1 = require("../../services/verification-token.service");
|
||||||
|
const password_reset_token_service_1 = require("../../services/password-reset-token.service");
|
||||||
|
const email_service_1 = require("../../services/email.service");
|
||||||
|
const settings_service_1 = require("../settings/settings.service");
|
||||||
|
const env_1 = require("../../config/env");
|
||||||
|
const logger_1 = require("../../utils/logger");
|
||||||
|
const auth_rate_limits_1 = require("./auth.rate-limits");
|
||||||
|
const profile_service_1 = require("../people/profile.service");
|
||||||
const router = (0, express_1.Router)();
|
const router = (0, express_1.Router)();
|
||||||
exports.authRouter = router;
|
exports.authRouter = router;
|
||||||
// POST /api/auth/login
|
// POST /api/auth/login
|
||||||
@ -62,6 +44,138 @@ router.post('/register', rate_limit_1.authRateLimit, (0, validate_1.validate)(au
|
|||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// POST /api/auth/verify-email
|
||||||
|
const verifyEmailSchema = zod_1.z.object({ token: zod_1.z.string().min(1) });
|
||||||
|
router.post('/verify-email', rate_limit_1.authRateLimit, (0, validate_1.validate)(verifyEmailSchema), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { token } = req.body;
|
||||||
|
const result = await verification_token_service_1.verificationTokenService.verifyToken(token);
|
||||||
|
if (!result.valid || !result.userId) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: { message: result.error || 'Invalid token', code: 'INVALID_TOKEN' },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const settings = await settings_service_1.siteSettingsService.get();
|
||||||
|
const autoApprove = settings.autoApproveVerifiedUsers;
|
||||||
|
const newStatus = autoApprove ? client_1.UserStatus.ACTIVE : client_1.UserStatus.PENDING_APPROVAL;
|
||||||
|
await database_1.prisma.user.update({
|
||||||
|
where: { id: result.userId },
|
||||||
|
data: { emailVerified: true, status: newStatus },
|
||||||
|
});
|
||||||
|
// If not auto-approved, notify admins
|
||||||
|
if (!autoApprove) {
|
||||||
|
const user = await database_1.prisma.user.findUnique({ where: { id: result.userId } });
|
||||||
|
if (user) {
|
||||||
|
const admins = await database_1.prisma.user.findMany({
|
||||||
|
where: { role: client_1.UserRole.SUPER_ADMIN, status: client_1.UserStatus.ACTIVE },
|
||||||
|
select: { email: true },
|
||||||
|
});
|
||||||
|
if (admins.length > 0) {
|
||||||
|
await email_service_1.emailService.sendPendingApprovalNotification({
|
||||||
|
adminEmails: admins.map(a => a.email),
|
||||||
|
newUserEmail: user.email,
|
||||||
|
newUserName: user.name || '',
|
||||||
|
}).catch(err => logger_1.logger.error('Failed to send approval notification:', err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
verified: true,
|
||||||
|
approved: autoApprove,
|
||||||
|
message: autoApprove
|
||||||
|
? 'Email verified. You can now log in.'
|
||||||
|
: 'Email verified. Your account is pending admin approval.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// POST /api/auth/resend-verification
|
||||||
|
const resendVerificationSchema = zod_1.z.object({ email: zod_1.z.string().email() });
|
||||||
|
router.post('/resend-verification', (0, auth_rate_limits_1.createVerificationRateLimit)(), (0, validate_1.validate)(resendVerificationSchema), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
// Always return success to prevent user enumeration
|
||||||
|
res.json({ message: 'If your email is registered and pending verification, a new verification link has been sent.' });
|
||||||
|
// Send asynchronously (don't block response)
|
||||||
|
const user = await database_1.prisma.user.findUnique({ where: { email: req.body.email } });
|
||||||
|
if (user && user.status === client_1.UserStatus.PENDING_VERIFICATION) {
|
||||||
|
const token = await verification_token_service_1.verificationTokenService.createToken(user.id);
|
||||||
|
const adminUrl = env_1.env.ADMIN_URL || 'http://localhost:3000';
|
||||||
|
const verificationUrl = `${adminUrl}/verify-email?token=${token}`;
|
||||||
|
await email_service_1.emailService.sendVerificationEmail({
|
||||||
|
recipientEmail: user.email,
|
||||||
|
recipientName: user.name || 'there',
|
||||||
|
verificationUrl,
|
||||||
|
}).catch(err => logger_1.logger.error('Failed to resend verification email:', err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// POST /api/auth/forgot-password
|
||||||
|
const forgotPasswordSchema = zod_1.z.object({ email: zod_1.z.string().email() });
|
||||||
|
router.post('/forgot-password', (0, auth_rate_limits_1.createResetRateLimit)(), (0, validate_1.validate)(forgotPasswordSchema), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
// Always return success to prevent user enumeration
|
||||||
|
res.json({ message: 'If your email is registered, a password reset link has been sent.' });
|
||||||
|
// Send asynchronously
|
||||||
|
const user = await database_1.prisma.user.findUnique({ where: { email: req.body.email } });
|
||||||
|
if (user && user.status === client_1.UserStatus.ACTIVE) {
|
||||||
|
const token = await password_reset_token_service_1.passwordResetTokenService.createToken(user.id);
|
||||||
|
const adminUrl = env_1.env.ADMIN_URL || 'http://localhost:3000';
|
||||||
|
const resetUrl = `${adminUrl}/reset-password?token=${token}`;
|
||||||
|
await email_service_1.emailService.sendPasswordResetEmail({
|
||||||
|
recipientEmail: user.email,
|
||||||
|
recipientName: user.name || 'there',
|
||||||
|
resetUrl,
|
||||||
|
}).catch(err => logger_1.logger.error('Failed to send password reset email:', err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// POST /api/auth/reset-password
|
||||||
|
const resetPasswordSchema = zod_1.z.object({
|
||||||
|
token: zod_1.z.string().min(1),
|
||||||
|
password: zod_1.z.string()
|
||||||
|
.min(12, 'Password must be at least 12 characters')
|
||||||
|
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||||
|
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||||
|
.regex(/[0-9]/, 'Password must contain at least one digit'),
|
||||||
|
});
|
||||||
|
router.post('/reset-password', rate_limit_1.authRateLimit, (0, validate_1.validate)(resetPasswordSchema), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { token, password } = req.body;
|
||||||
|
const result = await password_reset_token_service_1.passwordResetTokenService.validateToken(token);
|
||||||
|
if (!result.valid || !result.userId) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: { message: result.error || 'Invalid token', code: 'INVALID_TOKEN' },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hashedPassword = await bcryptjs_1.default.hash(password, 12);
|
||||||
|
// Update password, mark token used, invalidate all refresh tokens — all in one transaction
|
||||||
|
await database_1.prisma.$transaction(async (tx) => {
|
||||||
|
await tx.user.update({
|
||||||
|
where: { id: result.userId },
|
||||||
|
data: { password: hashedPassword },
|
||||||
|
});
|
||||||
|
await tx.refreshToken.deleteMany({ where: { userId: result.userId } });
|
||||||
|
await tx.passwordResetToken.update({
|
||||||
|
where: { token },
|
||||||
|
data: { usedAt: new Date() },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
res.json({ message: 'Password has been reset. You can now log in with your new password.' });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
// POST /api/auth/refresh
|
// POST /api/auth/refresh
|
||||||
router.post('/refresh', rate_limit_1.authRateLimit, (0, validate_1.validate)(auth_schemas_1.refreshSchema), async (req, res, next) => {
|
router.post('/refresh', rate_limit_1.authRateLimit, (0, validate_1.validate)(auth_schemas_1.refreshSchema), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
@ -82,11 +196,59 @@ router.post('/logout', rate_limit_1.authRateLimit, (0, validate_1.validate)(auth
|
|||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// GET /api/auth/me/profile-token
|
||||||
|
router.get('/me/profile-token', auth_middleware_1.authenticate, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
// Look up existing Contact linked to this user
|
||||||
|
let contact = await database_1.prisma.contact.findUnique({ where: { userId } });
|
||||||
|
// Auto-create Contact if none exists
|
||||||
|
if (!contact) {
|
||||||
|
const user = await database_1.prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
select: { name: true, email: true },
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
res.status(401).json({ error: { message: 'Invalid token', code: 'INVALID_TOKEN' } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
contact = await database_1.prisma.contact.create({
|
||||||
|
data: {
|
||||||
|
displayName: user.name || user.email,
|
||||||
|
firstName: user.name?.split(' ')[0] || null,
|
||||||
|
lastName: user.name?.split(' ').slice(1).join(' ') || null,
|
||||||
|
email: user.email,
|
||||||
|
userId,
|
||||||
|
primarySource: 'USER',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
// Race condition: another request created the Contact — retry lookup
|
||||||
|
if (err.code === 'P2002') {
|
||||||
|
contact = await database_1.prisma.contact.findUnique({ where: { userId } });
|
||||||
|
}
|
||||||
|
if (!contact)
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Generate profile token if Contact doesn't have one
|
||||||
|
if (!contact.profileToken) {
|
||||||
|
const result = await profile_service_1.profileService.generateProfileToken(contact.id);
|
||||||
|
res.json({ token: result.token });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.json({ token: contact.profileToken });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
// GET /api/auth/me
|
// GET /api/auth/me
|
||||||
router.get('/me', auth_middleware_1.authenticate, async (req, res, next) => {
|
router.get('/me', auth_middleware_1.authenticate, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { prisma } = await Promise.resolve().then(() => __importStar(require('../../config/database')));
|
const user = await database_1.prisma.user.findUnique({
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { id: req.user.id },
|
where: { id: req.user.id },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@ -94,6 +256,7 @@ router.get('/me', auth_middleware_1.authenticate, async (req, res, next) => {
|
|||||||
name: true,
|
name: true,
|
||||||
phone: true,
|
phone: true,
|
||||||
role: true,
|
role: true,
|
||||||
|
roles: true,
|
||||||
status: true,
|
status: true,
|
||||||
permissions: true,
|
permissions: true,
|
||||||
createdVia: true,
|
createdVia: true,
|
||||||
|
|||||||
2
api/dist/modules/auth/auth.routes.js.map
vendored
2
api/dist/modules/auth/auth.routes.js.map
vendored
File diff suppressed because one or more lines are too long
3
api/dist/modules/auth/auth.schemas.d.ts
vendored
3
api/dist/modules/auth/auth.schemas.d.ts
vendored
@ -14,16 +14,19 @@ export declare const registerSchema: z.ZodObject<{
|
|||||||
password: z.ZodString;
|
password: z.ZodString;
|
||||||
name: z.ZodOptional<z.ZodString>;
|
name: z.ZodOptional<z.ZodString>;
|
||||||
phone: z.ZodOptional<z.ZodString>;
|
phone: z.ZodOptional<z.ZodString>;
|
||||||
|
inviteCode: z.ZodOptional<z.ZodString>;
|
||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
name?: string | undefined;
|
name?: string | undefined;
|
||||||
phone?: string | undefined;
|
phone?: string | undefined;
|
||||||
|
inviteCode?: string | undefined;
|
||||||
}, {
|
}, {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
name?: string | undefined;
|
name?: string | undefined;
|
||||||
phone?: string | undefined;
|
phone?: string | undefined;
|
||||||
|
inviteCode?: string | undefined;
|
||||||
}>;
|
}>;
|
||||||
export declare const refreshSchema: z.ZodObject<{
|
export declare const refreshSchema: z.ZodObject<{
|
||||||
refreshToken: z.ZodString;
|
refreshToken: z.ZodString;
|
||||||
|
|||||||
2
api/dist/modules/auth/auth.schemas.d.ts.map
vendored
2
api/dist/modules/auth/auth.schemas.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"auth.schemas.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,WAAW;;;;;;;;;EAGtB,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;EAUzB,CAAC;AAEH,eAAO,MAAM,aAAa;;;;;;EAExB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AACrD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC"}
|
{"version":3,"file":"auth.schemas.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,WAAW;;;;;;;;;EAGtB,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;EAWzB,CAAC;AAEH,eAAO,MAAM,aAAa;;;;;;EAExB,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AACrD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC"}
|
||||||
1
api/dist/modules/auth/auth.schemas.js
vendored
1
api/dist/modules/auth/auth.schemas.js
vendored
@ -15,6 +15,7 @@ exports.registerSchema = zod_1.z.object({
|
|||||||
.regex(/[0-9]/, 'Password must contain at least one digit'),
|
.regex(/[0-9]/, 'Password must contain at least one digit'),
|
||||||
name: zod_1.z.string().optional(),
|
name: zod_1.z.string().optional(),
|
||||||
phone: zod_1.z.string().optional(),
|
phone: zod_1.z.string().optional(),
|
||||||
|
inviteCode: zod_1.z.string().max(20).optional(),
|
||||||
// Role removed from public registration - must be set server-side only
|
// Role removed from public registration - must be set server-side only
|
||||||
});
|
});
|
||||||
exports.refreshSchema = zod_1.z.object({
|
exports.refreshSchema = zod_1.z.object({
|
||||||
|
|||||||
2
api/dist/modules/auth/auth.schemas.js.map
vendored
2
api/dist/modules/auth/auth.schemas.js.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"auth.schemas.js","sourceRoot":"","sources":["../../../src/modules/auth/auth.schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAGX,QAAA,WAAW,GAAG,OAAC,CAAC,MAAM,CAAC;IAClC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IACzB,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,sBAAsB,CAAC;CACpD,CAAC,CAAC;AAEU,QAAA,cAAc,GAAG,OAAC,CAAC,MAAM,CAAC;IACrC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IACzB,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE;SACjB,GAAG,CAAC,EAAE,EAAE,yCAAyC,CAAC;SAClD,KAAK,CAAC,OAAO,EAAE,qDAAqD,CAAC;SACrE,KAAK,CAAC,OAAO,EAAE,qDAAqD,CAAC;SACrE,KAAK,CAAC,OAAO,EAAE,0CAA0C,CAAC;IAC7D,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,uEAAuE;CACxE,CAAC,CAAC;AAEU,QAAA,aAAa,GAAG,OAAC,CAAC,MAAM,CAAC;IACpC,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,2BAA2B,CAAC;CAC7D,CAAC,CAAC"}
|
{"version":3,"file":"auth.schemas.js","sourceRoot":"","sources":["../../../src/modules/auth/auth.schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AAGX,QAAA,WAAW,GAAG,OAAC,CAAC,MAAM,CAAC;IAClC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IACzB,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,sBAAsB,CAAC;CACpD,CAAC,CAAC;AAEU,QAAA,cAAc,GAAG,OAAC,CAAC,MAAM,CAAC;IACrC,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IACzB,QAAQ,EAAE,OAAC,CAAC,MAAM,EAAE;SACjB,GAAG,CAAC,EAAE,EAAE,yCAAyC,CAAC;SAClD,KAAK,CAAC,OAAO,EAAE,qDAAqD,CAAC;SACrE,KAAK,CAAC,OAAO,EAAE,qDAAqD,CAAC;SACrE,KAAK,CAAC,OAAO,EAAE,0CAA0C,CAAC;IAC7D,IAAI,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,KAAK,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IACzC,uEAAuE;CACxE,CAAC,CAAC;AAEU,QAAA,aAAa,GAAG,OAAC,CAAC,MAAM,CAAC;IACpC,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,2BAA2B,CAAC;CAC7D,CAAC,CAAC"}
|
||||||
59
api/dist/modules/auth/auth.service.d.ts
vendored
59
api/dist/modules/auth/auth.service.d.ts
vendored
@ -1,9 +1,15 @@
|
|||||||
import { UserRole } from '@prisma/client';
|
import { UserRole } from '@prisma/client';
|
||||||
import type { RegisterInput } from './auth.schemas';
|
import type { RegisterInput } from './auth.schemas';
|
||||||
interface TokenPair {
|
export interface TokenPair {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
}
|
}
|
||||||
|
type UserForToken = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
role: UserRole;
|
||||||
|
roles?: unknown;
|
||||||
|
};
|
||||||
export declare const authService: {
|
export declare const authService: {
|
||||||
login(email: string, password: string): Promise<{
|
login(email: string, password: string): Promise<{
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@ -14,7 +20,9 @@ export declare const authService: {
|
|||||||
email: string;
|
email: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
phone: string | null;
|
phone: string | null;
|
||||||
|
pronouns: string | null;
|
||||||
role: import(".prisma/client").$Enums.UserRole;
|
role: import(".prisma/client").$Enums.UserRole;
|
||||||
|
roles: import("@prisma/client/runtime/library").JsonValue;
|
||||||
permissions: import("@prisma/client/runtime/library").JsonValue | null;
|
permissions: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
createdVia: import(".prisma/client").$Enums.UserCreatedVia;
|
createdVia: import(".prisma/client").$Enums.UserCreatedVia;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
@ -26,15 +34,15 @@ export declare const authService: {
|
|||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
register(data: RegisterInput): Promise<{
|
register(data: RegisterInput): Promise<{
|
||||||
accessToken: string;
|
|
||||||
refreshToken: string;
|
|
||||||
user: {
|
user: {
|
||||||
status: import(".prisma/client").$Enums.UserStatus;
|
status: import(".prisma/client").$Enums.UserStatus;
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
phone: string | null;
|
phone: string | null;
|
||||||
|
pronouns: string | null;
|
||||||
role: import(".prisma/client").$Enums.UserRole;
|
role: import(".prisma/client").$Enums.UserRole;
|
||||||
|
roles: import("@prisma/client/runtime/library").JsonValue;
|
||||||
permissions: import("@prisma/client/runtime/library").JsonValue | null;
|
permissions: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
createdVia: import(".prisma/client").$Enums.UserCreatedVia;
|
createdVia: import(".prisma/client").$Enums.UserCreatedVia;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
@ -44,6 +52,31 @@ export declare const authService: {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
};
|
};
|
||||||
|
requiresVerification: boolean;
|
||||||
|
message: string;
|
||||||
|
} | {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
user: {
|
||||||
|
status: import(".prisma/client").$Enums.UserStatus;
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string | null;
|
||||||
|
phone: string | null;
|
||||||
|
pronouns: string | null;
|
||||||
|
role: import(".prisma/client").$Enums.UserRole;
|
||||||
|
roles: import("@prisma/client/runtime/library").JsonValue;
|
||||||
|
permissions: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
|
createdVia: import(".prisma/client").$Enums.UserCreatedVia;
|
||||||
|
expiresAt: Date | null;
|
||||||
|
expireDays: number | null;
|
||||||
|
lastLoginAt: Date | null;
|
||||||
|
emailVerified: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
requiresVerification?: undefined;
|
||||||
|
message?: undefined;
|
||||||
}>;
|
}>;
|
||||||
refreshTokens(refreshToken: string): Promise<{
|
refreshTokens(refreshToken: string): Promise<{
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@ -54,7 +87,9 @@ export declare const authService: {
|
|||||||
email: string;
|
email: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
phone: string | null;
|
phone: string | null;
|
||||||
|
pronouns: string | null;
|
||||||
role: import(".prisma/client").$Enums.UserRole;
|
role: import(".prisma/client").$Enums.UserRole;
|
||||||
|
roles: import("@prisma/client/runtime/library").JsonValue;
|
||||||
permissions: import("@prisma/client/runtime/library").JsonValue | null;
|
permissions: import("@prisma/client/runtime/library").JsonValue | null;
|
||||||
createdVia: import(".prisma/client").$Enums.UserCreatedVia;
|
createdVia: import(".prisma/client").$Enums.UserCreatedVia;
|
||||||
expiresAt: Date | null;
|
expiresAt: Date | null;
|
||||||
@ -66,21 +101,9 @@ export declare const authService: {
|
|||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
logout(refreshToken: string): Promise<void>;
|
logout(refreshToken: string): Promise<void>;
|
||||||
generateAccessToken(user: {
|
generateAccessToken(user: UserForToken): string;
|
||||||
id: string;
|
generateRefreshToken(user: UserForToken): Promise<string>;
|
||||||
email: string;
|
generateTokenPair(user: UserForToken): Promise<TokenPair>;
|
||||||
role: UserRole;
|
|
||||||
}): string;
|
|
||||||
generateRefreshToken(user: {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
role: UserRole;
|
|
||||||
}): Promise<string>;
|
|
||||||
generateTokenPair(user: {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
role: UserRole;
|
|
||||||
}): Promise<TokenPair>;
|
|
||||||
};
|
};
|
||||||
export {};
|
export {};
|
||||||
//# sourceMappingURL=auth.service.d.ts.map
|
//# sourceMappingURL=auth.service.d.ts.map
|
||||||
2
api/dist/modules/auth/auth.service.d.ts.map
vendored
2
api/dist/modules/auth/auth.service.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"auth.service.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAc,MAAM,gBAAgB,CAAC;AAKtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAQpD,UAAU,SAAS;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,WAAW;iBACH,MAAM,YAAY,MAAM;qBAL9B,MAAM;sBACL,MAAM;;;;;;;;;;;;;;;;;;mBAwCC,aAAa;qBAzCrB,MAAM;sBACL,MAAM;;;;;;;;;;;;;;;;;;gCAgEc,MAAM;;;;;;;;;;;;;;;;;;;;yBAwDb,MAAM;8BAIP;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,GAAG,MAAM;+BAO/C;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;4BAqBlE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,CAAC;CAKjG,CAAC"}
|
{"version":3,"file":"auth.service.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAc,MAAM,gBAAgB,CAAC;AAUtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AASpD,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,KAAK,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAUnF,eAAO,MAAM,WAAW;iBACH,MAAM,YAAY,MAAM;qBAf9B,MAAM;sBACL,MAAM;;;;;;;;;;;;;;;;;;;;mBA+EC,aAAa;;;;;;;;;;;;;;;;;;;;;;qBAhFrB,MAAM;sBACL,MAAM;;;;;;;;;;;;;;;;;;;;;;gCAiLc,MAAM;;;;;;;;;;;;;;;;;;;;;;yBAsEb,MAAM;8BAIP,YAAY,GAAG,MAAM;+BAcd,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;4BA2BjC,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC;CAKhE,CAAC"}
|
||||||
179
api/dist/modules/auth/auth.service.js
vendored
179
api/dist/modules/auth/auth.service.js
vendored
@ -1,4 +1,37 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
@ -11,6 +44,18 @@ const database_1 = require("../../config/database");
|
|||||||
const env_1 = require("../../config/env");
|
const env_1 = require("../../config/env");
|
||||||
const error_handler_1 = require("../../middleware/error-handler");
|
const error_handler_1 = require("../../middleware/error-handler");
|
||||||
const metrics_1 = require("../../utils/metrics");
|
const metrics_1 = require("../../utils/metrics");
|
||||||
|
const settings_service_1 = require("../settings/settings.service");
|
||||||
|
const verification_token_service_1 = require("../../services/verification-token.service");
|
||||||
|
const email_service_1 = require("../../services/email.service");
|
||||||
|
const roles_1 = require("../../utils/roles");
|
||||||
|
const logger_1 = require("../../utils/logger");
|
||||||
|
/** Parse the roles JSON field into a UserRole[] array */
|
||||||
|
function parseRoles(user) {
|
||||||
|
if (Array.isArray(user.roles) && user.roles.length > 0) {
|
||||||
|
return user.roles;
|
||||||
|
}
|
||||||
|
return [user.role];
|
||||||
|
}
|
||||||
exports.authService = {
|
exports.authService = {
|
||||||
async login(email, password) {
|
async login(email, password) {
|
||||||
const user = await database_1.prisma.user.findUnique({ where: { email } });
|
const user = await database_1.prisma.user.findUnique({ where: { email } });
|
||||||
@ -23,6 +68,15 @@ exports.authService = {
|
|||||||
(0, metrics_1.recordLoginAttempt)('failure');
|
(0, metrics_1.recordLoginAttempt)('failure');
|
||||||
throw new error_handler_1.AppError(401, 'Invalid email or password', 'INVALID_CREDENTIALS');
|
throw new error_handler_1.AppError(401, 'Invalid email or password', 'INVALID_CREDENTIALS');
|
||||||
}
|
}
|
||||||
|
// Status-specific errors
|
||||||
|
if (user.status === client_1.UserStatus.PENDING_VERIFICATION) {
|
||||||
|
(0, metrics_1.recordLoginAttempt)('failure');
|
||||||
|
throw new error_handler_1.AppError(403, 'Please verify your email address before logging in', 'EMAIL_NOT_VERIFIED');
|
||||||
|
}
|
||||||
|
if (user.status === client_1.UserStatus.PENDING_APPROVAL) {
|
||||||
|
(0, metrics_1.recordLoginAttempt)('failure');
|
||||||
|
throw new error_handler_1.AppError(403, 'Your account is pending admin approval', 'ACCOUNT_PENDING');
|
||||||
|
}
|
||||||
if (user.status !== client_1.UserStatus.ACTIVE) {
|
if (user.status !== client_1.UserStatus.ACTIVE) {
|
||||||
(0, metrics_1.recordLoginAttempt)('failure');
|
(0, metrics_1.recordLoginAttempt)('failure');
|
||||||
throw new error_handler_1.AppError(403, `Account is ${user.status.toLowerCase()}`, 'ACCOUNT_INACTIVE');
|
throw new error_handler_1.AppError(403, `Account is ${user.status.toLowerCase()}`, 'ACCOUNT_INACTIVE');
|
||||||
@ -36,25 +90,111 @@ exports.authService = {
|
|||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { lastLoginAt: new Date() },
|
data: { lastLoginAt: new Date() },
|
||||||
});
|
});
|
||||||
|
// Fire-and-forget: log USER_LOGIN activity on linked Contact
|
||||||
|
database_1.prisma.contact.findFirst({
|
||||||
|
where: { userId: user.id, mergedIntoId: null },
|
||||||
|
}).then(async (contact) => {
|
||||||
|
if (contact) {
|
||||||
|
await database_1.prisma.contactActivity.create({
|
||||||
|
data: {
|
||||||
|
contactId: contact.id,
|
||||||
|
type: 'USER_LOGIN',
|
||||||
|
title: 'User logged in',
|
||||||
|
description: `Login from ${user.email}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
logger_1.logger.warn('Login activity logging failed:', err);
|
||||||
|
});
|
||||||
const tokens = await this.generateTokenPair(user);
|
const tokens = await this.generateTokenPair(user);
|
||||||
const { password: _, ...userWithoutPassword } = user;
|
const { password: _, ...userWithoutPassword } = user;
|
||||||
return { user: userWithoutPassword, ...tokens };
|
return { user: userWithoutPassword, ...tokens };
|
||||||
},
|
},
|
||||||
async register(data) {
|
async register(data) {
|
||||||
|
// Check if public registration is enabled
|
||||||
|
const settings = await settings_service_1.siteSettingsService.get();
|
||||||
|
if (!settings.enablePublicRegistration) {
|
||||||
|
throw new error_handler_1.AppError(403, 'Public registration is currently disabled', 'REGISTRATION_DISABLED');
|
||||||
|
}
|
||||||
const existing = await database_1.prisma.user.findUnique({ where: { email: data.email } });
|
const existing = await database_1.prisma.user.findUnique({ where: { email: data.email } });
|
||||||
if (existing) {
|
if (existing) {
|
||||||
throw new error_handler_1.AppError(409, 'Email already registered', 'EMAIL_EXISTS');
|
throw new error_handler_1.AppError(409, 'Email already registered', 'EMAIL_EXISTS');
|
||||||
}
|
}
|
||||||
const hashedPassword = await bcryptjs_1.default.hash(data.password, 12);
|
const hashedPassword = await bcryptjs_1.default.hash(data.password, 12);
|
||||||
|
// Determine if email verification is needed
|
||||||
|
const smtpReady = await email_service_1.emailService.isSmtpConfigured();
|
||||||
|
const requireVerification = settings.enableEmailVerification && smtpReady;
|
||||||
const user = await database_1.prisma.user.create({
|
const user = await database_1.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
email: data.email,
|
email: data.email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
phone: data.phone,
|
phone: data.phone,
|
||||||
role: client_1.UserRole.USER, // Always USER for public registration
|
role: client_1.UserRole.USER,
|
||||||
|
roles: JSON.parse(JSON.stringify([client_1.UserRole.USER])),
|
||||||
|
status: requireVerification ? client_1.UserStatus.PENDING_VERIFICATION : client_1.UserStatus.ACTIVE,
|
||||||
|
emailVerified: !requireVerification,
|
||||||
|
createdVia: 'SELF_REGISTRATION',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Fire-and-forget: process referral if invite code provided
|
||||||
|
if (data.inviteCode) {
|
||||||
|
Promise.resolve().then(() => __importStar(require('../social/referral.service'))).then(({ referralService }) => {
|
||||||
|
referralService.processRegistrationReferral(user.id, data.inviteCode).catch(err => {
|
||||||
|
logger_1.logger.warn('Referral processing failed:', err);
|
||||||
|
});
|
||||||
|
}).catch(() => { });
|
||||||
|
}
|
||||||
|
// Fire-and-forget: auto-link or create Contact if People feature is enabled
|
||||||
|
settings_service_1.siteSettingsService.get().then(async (s) => {
|
||||||
|
if (!s.enablePeople)
|
||||||
|
return;
|
||||||
|
// Check for existing Contact with matching email → link
|
||||||
|
const existingContact = await database_1.prisma.contact.findFirst({
|
||||||
|
where: { email: { equals: data.email, mode: 'insensitive' }, userId: null, mergedIntoId: null },
|
||||||
|
});
|
||||||
|
if (existingContact) {
|
||||||
|
await database_1.prisma.contact.update({ where: { id: existingContact.id }, data: { userId: user.id } });
|
||||||
|
logger_1.logger.info(`Auto-linked contact ${existingContact.id} to registered user ${user.id}`);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Create new Contact linked to the user
|
||||||
|
await database_1.prisma.contact.create({
|
||||||
|
data: {
|
||||||
|
displayName: data.name || data.email,
|
||||||
|
firstName: data.name?.split(' ')[0] || null,
|
||||||
|
lastName: data.name?.split(' ').slice(1).join(' ') || null,
|
||||||
|
email: data.email,
|
||||||
|
phone: data.phone || null,
|
||||||
|
primarySource: 'USER',
|
||||||
|
userId: user.id,
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
logger_1.logger.info(`Auto-created contact for registered user ${user.id}`);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
logger_1.logger.warn('Contact auto-creation on register failed:', err);
|
||||||
|
});
|
||||||
|
// If verification required, send email and don't issue tokens
|
||||||
|
if (requireVerification) {
|
||||||
|
const token = await verification_token_service_1.verificationTokenService.createToken(user.id);
|
||||||
|
const adminUrl = env_1.env.ADMIN_URL || 'http://localhost:3000';
|
||||||
|
const verificationUrl = `${adminUrl}/verify-email?token=${token}`;
|
||||||
|
await email_service_1.emailService.sendVerificationEmail({
|
||||||
|
recipientEmail: user.email,
|
||||||
|
recipientName: user.name || 'there',
|
||||||
|
verificationUrl,
|
||||||
|
});
|
||||||
|
const { password: _, ...userWithoutPassword } = user;
|
||||||
|
return {
|
||||||
|
user: userWithoutPassword,
|
||||||
|
requiresVerification: true,
|
||||||
|
message: 'Please check your email to verify your account',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// No verification needed — issue tokens immediately
|
||||||
const tokens = await this.generateTokenPair(user);
|
const tokens = await this.generateTokenPair(user);
|
||||||
const { password: _, ...userWithoutPassword } = user;
|
const { password: _, ...userWithoutPassword } = user;
|
||||||
return { user: userWithoutPassword, ...tokens };
|
return { user: userWithoutPassword, ...tokens };
|
||||||
@ -62,7 +202,7 @@ exports.authService = {
|
|||||||
async refreshTokens(refreshToken) {
|
async refreshTokens(refreshToken) {
|
||||||
let payload;
|
let payload;
|
||||||
try {
|
try {
|
||||||
payload = jsonwebtoken_1.default.verify(refreshToken, env_1.env.JWT_REFRESH_SECRET);
|
payload = jsonwebtoken_1.default.verify(refreshToken, env_1.env.JWT_REFRESH_SECRET, { algorithms: ['HS256'] });
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
throw new error_handler_1.AppError(401, 'Invalid refresh token', 'INVALID_REFRESH_TOKEN');
|
throw new error_handler_1.AppError(401, 'Invalid refresh token', 'INVALID_REFRESH_TOKEN');
|
||||||
@ -74,6 +214,16 @@ exports.authService = {
|
|||||||
if (!stored) {
|
if (!stored) {
|
||||||
throw new error_handler_1.AppError(401, 'Refresh token not found', 'INVALID_REFRESH_TOKEN');
|
throw new error_handler_1.AppError(401, 'Refresh token not found', 'INVALID_REFRESH_TOKEN');
|
||||||
}
|
}
|
||||||
|
// Check user status — banned/inactive users must not get new tokens
|
||||||
|
if (stored.user.status !== client_1.UserStatus.ACTIVE) {
|
||||||
|
await database_1.prisma.refreshToken.delete({ where: { id: stored.id } });
|
||||||
|
throw new error_handler_1.AppError(401, 'Account is not active', 'ACCOUNT_INACTIVE');
|
||||||
|
}
|
||||||
|
// Check account expiry
|
||||||
|
if (stored.user.expiresAt && stored.user.expiresAt < new Date()) {
|
||||||
|
await database_1.prisma.refreshToken.delete({ where: { id: stored.id } });
|
||||||
|
throw new error_handler_1.AppError(401, 'Account has expired', 'ACCOUNT_EXPIRED');
|
||||||
|
}
|
||||||
if (stored.expiresAt < new Date()) {
|
if (stored.expiresAt < new Date()) {
|
||||||
await database_1.prisma.refreshToken.delete({ where: { id: stored.id } });
|
await database_1.prisma.refreshToken.delete({ where: { id: stored.id } });
|
||||||
throw new error_handler_1.AppError(401, 'Refresh token expired', 'REFRESH_TOKEN_EXPIRED');
|
throw new error_handler_1.AppError(401, 'Refresh token expired', 'REFRESH_TOKEN_EXPIRED');
|
||||||
@ -81,14 +231,16 @@ exports.authService = {
|
|||||||
// Rotate: delete old and create new atomically
|
// Rotate: delete old and create new atomically
|
||||||
const tokens = await database_1.prisma.$transaction(async (tx) => {
|
const tokens = await database_1.prisma.$transaction(async (tx) => {
|
||||||
await tx.refreshToken.delete({ where: { id: stored.id } });
|
await tx.refreshToken.delete({ where: { id: stored.id } });
|
||||||
// Generate new token pair
|
const userRoles = parseRoles(stored.user);
|
||||||
const accessToken = this.generateAccessToken(stored.user);
|
const accessToken = this.generateAccessToken(stored.user);
|
||||||
const refreshPayload = {
|
const refreshPayload = {
|
||||||
id: stored.user.id,
|
id: stored.user.id,
|
||||||
email: stored.user.email,
|
email: stored.user.email,
|
||||||
role: stored.user.role
|
role: (0, roles_1.getPrimaryRole)(userRoles),
|
||||||
|
roles: userRoles,
|
||||||
};
|
};
|
||||||
const refreshToken = jsonwebtoken_1.default.sign(refreshPayload, env_1.env.JWT_REFRESH_SECRET, {
|
const refreshToken = jsonwebtoken_1.default.sign(refreshPayload, env_1.env.JWT_REFRESH_SECRET, {
|
||||||
|
algorithm: 'HS256',
|
||||||
expiresIn: env_1.env.JWT_REFRESH_EXPIRY,
|
expiresIn: env_1.env.JWT_REFRESH_EXPIRY,
|
||||||
});
|
});
|
||||||
const decoded = jsonwebtoken_1.default.decode(refreshToken);
|
const decoded = jsonwebtoken_1.default.decode(refreshToken);
|
||||||
@ -109,17 +261,30 @@ exports.authService = {
|
|||||||
await database_1.prisma.refreshToken.deleteMany({ where: { token: refreshToken } });
|
await database_1.prisma.refreshToken.deleteMany({ where: { token: refreshToken } });
|
||||||
},
|
},
|
||||||
generateAccessToken(user) {
|
generateAccessToken(user) {
|
||||||
const payload = { id: user.id, email: user.email, role: user.role };
|
const userRoles = parseRoles(user);
|
||||||
|
const payload = {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
role: (0, roles_1.getPrimaryRole)(userRoles),
|
||||||
|
roles: userRoles,
|
||||||
|
};
|
||||||
return jsonwebtoken_1.default.sign(payload, env_1.env.JWT_ACCESS_SECRET, {
|
return jsonwebtoken_1.default.sign(payload, env_1.env.JWT_ACCESS_SECRET, {
|
||||||
|
algorithm: 'HS256',
|
||||||
expiresIn: env_1.env.JWT_ACCESS_EXPIRY,
|
expiresIn: env_1.env.JWT_ACCESS_EXPIRY,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async generateRefreshToken(user) {
|
async generateRefreshToken(user) {
|
||||||
const payload = { id: user.id, email: user.email, role: user.role };
|
const userRoles = parseRoles(user);
|
||||||
|
const payload = {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
role: (0, roles_1.getPrimaryRole)(userRoles),
|
||||||
|
roles: userRoles,
|
||||||
|
};
|
||||||
const token = jsonwebtoken_1.default.sign(payload, env_1.env.JWT_REFRESH_SECRET, {
|
const token = jsonwebtoken_1.default.sign(payload, env_1.env.JWT_REFRESH_SECRET, {
|
||||||
|
algorithm: 'HS256',
|
||||||
expiresIn: env_1.env.JWT_REFRESH_EXPIRY,
|
expiresIn: env_1.env.JWT_REFRESH_EXPIRY,
|
||||||
});
|
});
|
||||||
// Parse expiry to get a Date
|
|
||||||
const decoded = jsonwebtoken_1.default.decode(token);
|
const decoded = jsonwebtoken_1.default.decode(token);
|
||||||
const expiresAt = new Date(decoded.exp * 1000);
|
const expiresAt = new Date(decoded.exp * 1000);
|
||||||
await database_1.prisma.refreshToken.create({
|
await database_1.prisma.refreshToken.create({
|
||||||
|
|||||||
2
api/dist/modules/auth/auth.service.js.map
vendored
2
api/dist/modules/auth/auth.service.js.map
vendored
File diff suppressed because one or more lines are too long
14
api/dist/modules/docs/docs-files.service.d.ts
vendored
14
api/dist/modules/docs/docs-files.service.d.ts
vendored
@ -26,6 +26,17 @@ declare function createFile(relativePath: string, content?: string, isDirectory?
|
|||||||
declare function deleteFile(relativePath: string): Promise<void>;
|
declare function deleteFile(relativePath: string): Promise<void>;
|
||||||
declare function renameFile(fromPath: string, toPath: string): Promise<void>;
|
declare function renameFile(fromPath: string, toPath: string): Promise<void>;
|
||||||
declare function isEditableFile(relativePath: string): boolean;
|
declare function isEditableFile(relativePath: string): boolean;
|
||||||
|
declare function uploadFile(relativePath: string, sourcePath: string): Promise<void>;
|
||||||
|
declare function invalidateTreeCache(): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Flatten a FileNode tree into file-only entries, then filter by
|
||||||
|
* case-insensitive query match on name or path. Name matches score
|
||||||
|
* higher so they sort first.
|
||||||
|
*/
|
||||||
|
declare function searchFiles(query: string, limit?: number): Promise<{
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}[]>;
|
||||||
export declare const docsFilesService: {
|
export declare const docsFilesService: {
|
||||||
listTree: typeof listTree;
|
listTree: typeof listTree;
|
||||||
readFileContent: typeof readFileContent;
|
readFileContent: typeof readFileContent;
|
||||||
@ -33,8 +44,11 @@ export declare const docsFilesService: {
|
|||||||
createFile: typeof createFile;
|
createFile: typeof createFile;
|
||||||
deleteFile: typeof deleteFile;
|
deleteFile: typeof deleteFile;
|
||||||
renameFile: typeof renameFile;
|
renameFile: typeof renameFile;
|
||||||
|
uploadFile: typeof uploadFile;
|
||||||
safeResolve: typeof safeResolve;
|
safeResolve: typeof safeResolve;
|
||||||
isEditableFile: typeof isEditableFile;
|
isEditableFile: typeof isEditableFile;
|
||||||
|
invalidateTreeCache: typeof invalidateTreeCache;
|
||||||
|
searchFiles: typeof searchFiles;
|
||||||
};
|
};
|
||||||
export {};
|
export {};
|
||||||
//# sourceMappingURL=docs-files.service.d.ts.map
|
//# sourceMappingURL=docs-files.service.d.ts.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"docs-files.service.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/docs-files.service.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;CACvB;AAaD;;;GAGG;AACH,iBAAS,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED,qBAAa,kBAAmB,SAAQ,KAAK;;CAK5C;AAED,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,QAAQ,EAAE,MAAM;CAI7B;AAED;;;GAGG;AACH,iBAAe,QAAQ,CAAC,GAAG,GAAE,MAAkB,EAAE,OAAO,GAAE,MAAW,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAiD1F;AAED,iBAAe,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCpE;AAED,iBAAe,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYpF;AAED,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BtG;AAED,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8B7D;AAED,iBAAe,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE;AAED,iBAAS,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;CAS5B,CAAC"}
|
{"version":3,"file":"docs-files.service.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/docs-files.service.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;CACvB;AAcD;;;GAGG;AACH,iBAAS,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAQjD;AAED,qBAAa,kBAAmB,SAAQ,KAAK;;CAK5C;AAED,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,QAAQ,EAAE,MAAM;CAI7B;AAED;;;GAGG;AACH,iBAAe,QAAQ,CAAC,GAAG,GAAE,MAAkB,EAAE,OAAO,GAAE,MAAW,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAiD1F;AAED,iBAAe,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCpE;AAED,iBAAe,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYpF;AAED,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BtG;AAED,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8B7D;AAED,iBAAe,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE;AAED,iBAAS,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAGrD;AAOD,iBAAe,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBjF;AAED,iBAAe,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMlD;AAED;;;;GAIG;AACH,iBAAe,WAAW,CACxB,KAAK,EAAE,MAAM,EACb,KAAK,SAAI,GACR,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC,CAyB3C;AAED,eAAO,MAAM,gBAAgB;;;;;;;;;;;;CAY5B,CAAC"}
|
||||||
72
api/dist/modules/docs/docs-files.service.js
vendored
72
api/dist/modules/docs/docs-files.service.js
vendored
@ -15,7 +15,8 @@ const DOCS_ROOT = (0, path_1.resolve)(env_1.env.MKDOCS_DOCS_PATH);
|
|||||||
// Redis cache configuration
|
// Redis cache configuration
|
||||||
const CACHE_KEY_PREFIX = 'DOCS_CACHE:';
|
const CACHE_KEY_PREFIX = 'DOCS_CACHE:';
|
||||||
const TREE_CACHE_KEY = `${CACHE_KEY_PREFIX}tree`;
|
const TREE_CACHE_KEY = `${CACHE_KEY_PREFIX}tree`;
|
||||||
const FILE_CACHE_TTL = 60 * 60; // 1 hour
|
const TREE_CACHE_TTL = 30; // 30 seconds — short so external changes show quickly
|
||||||
|
const FILE_CONTENT_CACHE_TTL = 60 * 60; // 1 hour for file content
|
||||||
function hashFilePath(path) {
|
function hashFilePath(path) {
|
||||||
return crypto_1.default.createHash('sha256').update(path).digest('hex').substring(0, 16);
|
return crypto_1.default.createHash('sha256').update(path).digest('hex').substring(0, 16);
|
||||||
}
|
}
|
||||||
@ -26,7 +27,8 @@ function hashFilePath(path) {
|
|||||||
function safeResolve(relativePath) {
|
function safeResolve(relativePath) {
|
||||||
const normalized = (0, path_1.normalize)(relativePath).replace(/^(\.\.(\/|\\|$))+/, '');
|
const normalized = (0, path_1.normalize)(relativePath).replace(/^(\.\.(\/|\\|$))+/, '');
|
||||||
const resolved = (0, path_1.resolve)(DOCS_ROOT, normalized);
|
const resolved = (0, path_1.resolve)(DOCS_ROOT, normalized);
|
||||||
if (!resolved.startsWith(DOCS_ROOT)) {
|
// Use DOCS_ROOT + sep to prevent prefix attacks (e.g., /mkdocs/docs-evil matching /mkdocs/docs)
|
||||||
|
if (resolved !== DOCS_ROOT && !resolved.startsWith(DOCS_ROOT + '/')) {
|
||||||
throw new PathTraversalError();
|
throw new PathTraversalError();
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
@ -89,7 +91,7 @@ async function listTree(dir = DOCS_ROOT, relBase = '') {
|
|||||||
// Cache root result
|
// Cache root result
|
||||||
if (dir === DOCS_ROOT && !relBase) {
|
if (dir === DOCS_ROOT && !relBase) {
|
||||||
try {
|
try {
|
||||||
await redis_1.redis.setex(TREE_CACHE_KEY, FILE_CACHE_TTL, JSON.stringify(nodes));
|
await redis_1.redis.setex(TREE_CACHE_KEY, TREE_CACHE_TTL, JSON.stringify(nodes));
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
logger_1.logger.warn('Failed to cache docs tree:', err);
|
logger_1.logger.warn('Failed to cache docs tree:', err);
|
||||||
@ -118,7 +120,7 @@ async function readFileContent(relativePath) {
|
|||||||
const content = await (0, promises_1.readFile)(fullPath, 'utf-8');
|
const content = await (0, promises_1.readFile)(fullPath, 'utf-8');
|
||||||
// Cache the result
|
// Cache the result
|
||||||
try {
|
try {
|
||||||
await redis_1.redis.setex(cacheKey, FILE_CACHE_TTL, content);
|
await redis_1.redis.setex(cacheKey, FILE_CONTENT_CACHE_TTL, content);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
logger_1.logger.warn('Failed to cache file content:', err);
|
logger_1.logger.warn('Failed to cache file content:', err);
|
||||||
@ -235,6 +237,65 @@ function isEditableFile(relativePath) {
|
|||||||
const ext = (0, path_1.extname)(relativePath).toLowerCase();
|
const ext = (0, path_1.extname)(relativePath).toLowerCase();
|
||||||
return ['.md', '.txt', '.yml', '.yaml', '.json', '.css', '.html', '.js'].includes(ext);
|
return ['.md', '.txt', '.yml', '.yaml', '.json', '.css', '.html', '.js'].includes(ext);
|
||||||
}
|
}
|
||||||
|
const ALLOWED_UPLOAD_EXTENSIONS = new Set([
|
||||||
|
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
|
||||||
|
'.pdf', '.zip',
|
||||||
|
]);
|
||||||
|
async function uploadFile(relativePath, sourcePath) {
|
||||||
|
const ext = (0, path_1.extname)(relativePath).toLowerCase();
|
||||||
|
if (!ALLOWED_UPLOAD_EXTENSIONS.has(ext)) {
|
||||||
|
throw new Error(`File type not allowed: ${ext}`);
|
||||||
|
}
|
||||||
|
const fullPath = safeResolve(relativePath);
|
||||||
|
await (0, promises_1.mkdir)((0, path_1.dirname)(fullPath), { recursive: true });
|
||||||
|
await (0, promises_1.copyFile)(sourcePath, fullPath);
|
||||||
|
// Invalidate tree cache (structure changed)
|
||||||
|
try {
|
||||||
|
await redis_1.redis.del(TREE_CACHE_KEY);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
logger_1.logger.warn('Failed to invalidate tree cache after upload:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function invalidateTreeCache() {
|
||||||
|
try {
|
||||||
|
await redis_1.redis.del(TREE_CACHE_KEY);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
logger_1.logger.warn('Failed to invalidate tree cache:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Flatten a FileNode tree into file-only entries, then filter by
|
||||||
|
* case-insensitive query match on name or path. Name matches score
|
||||||
|
* higher so they sort first.
|
||||||
|
*/
|
||||||
|
async function searchFiles(query, limit = 5) {
|
||||||
|
const tree = await listTree();
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
const matches = [];
|
||||||
|
function walk(nodes) {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.isDirectory) {
|
||||||
|
if (node.children)
|
||||||
|
walk(node.children);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const nameLower = node.name.toLowerCase();
|
||||||
|
const pathLower = node.path.toLowerCase();
|
||||||
|
if (nameLower.includes(q)) {
|
||||||
|
matches.push({ name: node.name, path: node.path, score: 2 });
|
||||||
|
}
|
||||||
|
else if (pathLower.includes(q)) {
|
||||||
|
matches.push({ name: node.name, path: node.path, score: 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(tree);
|
||||||
|
matches.sort((a, b) => b.score - a.score);
|
||||||
|
return matches.slice(0, limit).map(({ name, path }) => ({ name, path }));
|
||||||
|
}
|
||||||
exports.docsFilesService = {
|
exports.docsFilesService = {
|
||||||
listTree,
|
listTree,
|
||||||
readFileContent,
|
readFileContent,
|
||||||
@ -242,7 +303,10 @@ exports.docsFilesService = {
|
|||||||
createFile,
|
createFile,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
renameFile,
|
renameFile,
|
||||||
|
uploadFile,
|
||||||
safeResolve,
|
safeResolve,
|
||||||
isEditableFile,
|
isEditableFile,
|
||||||
|
invalidateTreeCache,
|
||||||
|
searchFiles,
|
||||||
};
|
};
|
||||||
//# sourceMappingURL=docs-files.service.js.map
|
//# sourceMappingURL=docs-files.service.js.map
|
||||||
File diff suppressed because one or more lines are too long
2
api/dist/modules/docs/docs.routes.d.ts.map
vendored
2
api/dist/modules/docs/docs.routes.d.ts.map
vendored
@ -1 +1 @@
|
|||||||
{"version":3,"file":"docs.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/docs.routes.ts"],"names":[],"mappings":"AAiQA,eAAO,MAAM,UAAU,4CAAS,CAAC"}
|
{"version":3,"file":"docs.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/docs.routes.ts"],"names":[],"mappings":"AA4YA,eAAO,MAAM,UAAU,4CAAS,CAAC"}
|
||||||
151
api/dist/modules/docs/docs.routes.js
vendored
151
api/dist/modules/docs/docs.routes.js
vendored
@ -1,21 +1,31 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.docsRouter = void 0;
|
exports.docsRouter = void 0;
|
||||||
const express_1 = require("express");
|
const express_1 = require("express");
|
||||||
|
const multer_1 = __importDefault(require("multer"));
|
||||||
|
const promises_1 = require("fs/promises");
|
||||||
|
const path_1 = require("path");
|
||||||
const auth_middleware_1 = require("../../middleware/auth.middleware");
|
const auth_middleware_1 = require("../../middleware/auth.middleware");
|
||||||
const rbac_middleware_1 = require("../../middleware/rbac.middleware");
|
const rbac_middleware_1 = require("../../middleware/rbac.middleware");
|
||||||
const env_1 = require("../../config/env");
|
const env_1 = require("../../config/env");
|
||||||
|
const roles_1 = require("../../utils/roles");
|
||||||
const logger_1 = require("../../utils/logger");
|
const logger_1 = require("../../utils/logger");
|
||||||
const health_check_1 = require("../../utils/health-check");
|
const health_check_1 = require("../../utils/health-check");
|
||||||
const metrics_1 = require("../../utils/metrics");
|
const metrics_1 = require("../../utils/metrics");
|
||||||
const docs_files_service_1 = require("./docs-files.service");
|
const docs_files_service_1 = require("./docs-files.service");
|
||||||
|
const docs_collab_service_1 = require("./docs-collab.service");
|
||||||
const mkdocs_config_service_1 = require("./mkdocs-config.service");
|
const mkdocs_config_service_1 = require("./mkdocs-config.service");
|
||||||
|
const header_builder_service_1 = require("./header-builder.service");
|
||||||
|
const header_builder_schemas_1 = require("./header-builder.schemas");
|
||||||
const router = (0, express_1.Router)();
|
const router = (0, express_1.Router)();
|
||||||
router.use(auth_middleware_1.authenticate);
|
router.use(auth_middleware_1.authenticate);
|
||||||
router.use(rbac_middleware_1.requireNonTemp);
|
router.use(rbac_middleware_1.requireNonTemp);
|
||||||
// Removed duplicated isServiceOnline - now using shared utility from utils/health-check.ts
|
// Removed duplicated isServiceOnline - now using shared utility from utils/health-check.ts
|
||||||
// GET /api/docs/status — check MkDocs and Code Server availability
|
// GET /api/docs/status — check MkDocs and Code Server availability (content editors only)
|
||||||
router.get('/status', async (_req, res, next) => {
|
router.get('/status', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const [mkdocsOnline, codeServerOnline, siteServerOnline] = await Promise.all([
|
const [mkdocsOnline, codeServerOnline, siteServerOnline] = await Promise.all([
|
||||||
(0, health_check_1.isServiceOnline)(env_1.env.MKDOCS_PREVIEW_URL),
|
(0, health_check_1.isServiceOnline)(env_1.env.MKDOCS_PREVIEW_URL),
|
||||||
@ -33,8 +43,8 @@ router.get('/status', async (_req, res, next) => {
|
|||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// GET /api/docs/config — return public-facing port numbers for iframe URLs
|
// GET /api/docs/config — return public-facing port numbers for iframe URLs (content editors only)
|
||||||
router.get('/config', async (_req, res, _next) => {
|
router.get('/config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, _next) => {
|
||||||
res.json({
|
res.json({
|
||||||
codeServerPort: env_1.env.CODE_SERVER_PORT,
|
codeServerPort: env_1.env.CODE_SERVER_PORT,
|
||||||
mkdocsPort: env_1.env.MKDOCS_PORT,
|
mkdocsPort: env_1.env.MKDOCS_PORT,
|
||||||
@ -42,8 +52,8 @@ router.get('/config', async (_req, res, _next) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
// --- MkDocs Config Endpoints ---
|
// --- MkDocs Config Endpoints ---
|
||||||
// GET /api/docs/mkdocs-config — read raw mkdocs.yml content
|
// GET /api/docs/mkdocs-config — read raw mkdocs.yml content (content editors only)
|
||||||
router.get('/mkdocs-config', async (_req, res, next) => {
|
router.get('/mkdocs-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const content = await mkdocs_config_service_1.mkdocsConfigService.readConfig();
|
const content = await mkdocs_config_service_1.mkdocsConfigService.readConfig();
|
||||||
res.json({ content });
|
res.json({ content });
|
||||||
@ -54,7 +64,7 @@ router.get('/mkdocs-config', async (_req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// PUT /api/docs/mkdocs-config — validate + write mkdocs.yml (SUPER_ADMIN only)
|
// PUT /api/docs/mkdocs-config — validate + write mkdocs.yml (SUPER_ADMIN only)
|
||||||
router.put('/mkdocs-config', (0, rbac_middleware_1.requireRole)('SUPER_ADMIN'), async (req, res, next) => {
|
router.put('/mkdocs-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { content } = req.body;
|
const { content } = req.body;
|
||||||
if (typeof content !== 'string') {
|
if (typeof content !== 'string') {
|
||||||
@ -73,8 +83,8 @@ router.put('/mkdocs-config', (0, rbac_middleware_1.requireRole)('SUPER_ADMIN'),
|
|||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// POST /api/docs/build — trigger mkdocs build in container (SUPER_ADMIN only)
|
// POST /api/docs/build — trigger mkdocs build in container
|
||||||
router.post('/build', (0, rbac_middleware_1.requireRole)('SUPER_ADMIN'), async (_req, res, next) => {
|
router.post('/build', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const result = await mkdocs_config_service_1.mkdocsConfigService.triggerBuild();
|
const result = await mkdocs_config_service_1.mkdocsConfigService.triggerBuild();
|
||||||
res.json(result);
|
res.json(result);
|
||||||
@ -84,11 +94,95 @@ router.post('/build', (0, rbac_middleware_1.requireRole)('SUPER_ADMIN'), async (
|
|||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// --- Header Builder ---
|
||||||
|
// GET /api/docs/header-config — read header nav bar config (content editors only)
|
||||||
|
router.get('/header-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
|
||||||
|
try {
|
||||||
|
const config = await header_builder_service_1.headerBuilderService.readConfig();
|
||||||
|
res.json(config);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
logger_1.logger.error('Failed to read header config', err);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// PUT /api/docs/header-config — save header nav bar config + regenerate template
|
||||||
|
router.put('/header-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const parsed = header_builder_schemas_1.headerConfigSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: { message: 'Invalid header config', code: 'VALIDATION_ERROR', details: parsed.error.flatten().fieldErrors },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await header_builder_service_1.headerBuilderService.writeConfig(parsed.data);
|
||||||
|
// Invalidate docs file tree cache so the new main.html shows up
|
||||||
|
await docs_files_service_1.docsFilesService.invalidateTreeCache();
|
||||||
|
res.json({ success: true });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
logger_1.logger.error('Failed to save header config', err);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// --- File Upload ---
|
||||||
|
const ALLOWED_UPLOAD_EXTENSIONS = new Set([
|
||||||
|
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
|
||||||
|
'.pdf', '.zip',
|
||||||
|
]);
|
||||||
|
const upload = (0, multer_1.default)({
|
||||||
|
storage: multer_1.default.diskStorage({}), // temp dir
|
||||||
|
limits: { fileSize: 20 * 1024 * 1024 }, // 20MB
|
||||||
|
fileFilter: (_req, file, cb) => {
|
||||||
|
const ext = (0, path_1.extname)(file.originalname).toLowerCase();
|
||||||
|
if (ALLOWED_UPLOAD_EXTENSIONS.has(ext)) {
|
||||||
|
cb(null, true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cb(new Error(`File type not allowed: ${ext}`));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// POST /api/docs/upload — upload binary file (image, pdf, etc.)
|
||||||
|
router.post('/upload', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), upload.single('file'), async (req, res, next) => {
|
||||||
|
const tempPath = req.file?.path;
|
||||||
|
try {
|
||||||
|
metrics_1.cm_docs_operations.inc({ operation: 'upload' });
|
||||||
|
if (!req.file) {
|
||||||
|
res.status(400).json({ error: { message: 'No file provided', code: 'VALIDATION_ERROR' } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetDir = req.body.path || '';
|
||||||
|
const fileName = (0, path_1.basename)(req.file.originalname).replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||||
|
const relativePath = targetDir ? `${targetDir}/${fileName}` : fileName;
|
||||||
|
await docs_files_service_1.docsFilesService.uploadFile(relativePath, req.file.path);
|
||||||
|
// Clean up temp file
|
||||||
|
try {
|
||||||
|
await (0, promises_1.rm)(req.file.path);
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
res.json({ success: true, path: relativePath });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
// Clean up temp file on error
|
||||||
|
if (tempPath) {
|
||||||
|
try {
|
||||||
|
await (0, promises_1.rm)(tempPath);
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
handleFileError(err, res, next);
|
||||||
|
}
|
||||||
|
});
|
||||||
// --- File Management Endpoints ---
|
// --- File Management Endpoints ---
|
||||||
// GET /api/docs/files — list file tree
|
// GET /api/docs/files — list file tree (content editors only)
|
||||||
router.get('/files', async (_req, res, next) => {
|
router.get('/files', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
metrics_1.cm_docs_operations.inc({ operation: 'list' });
|
metrics_1.cm_docs_operations.inc({ operation: 'list' });
|
||||||
|
if (req.query['force'] === 'true') {
|
||||||
|
await docs_files_service_1.docsFilesService.invalidateTreeCache();
|
||||||
|
}
|
||||||
const tree = await docs_files_service_1.docsFilesService.listTree();
|
const tree = await docs_files_service_1.docsFilesService.listTree();
|
||||||
res.json(tree);
|
res.json(tree);
|
||||||
}
|
}
|
||||||
@ -97,8 +191,25 @@ router.get('/files', async (_req, res, next) => {
|
|||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// GET /api/docs/files/search — search files by name/path (content editors only)
|
||||||
|
router.get('/files/search', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const search = String(req.query['search'] ?? req.query['q'] ?? '').trim();
|
||||||
|
if (!search) {
|
||||||
|
res.json({ files: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const limit = Math.min(Math.max(Number(req.query['limit']) || 5, 1), 20);
|
||||||
|
const files = await docs_files_service_1.docsFilesService.searchFiles(search, limit);
|
||||||
|
res.json({ files });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
logger_1.logger.error('Failed to search docs files', err);
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
// POST /api/docs/files/rename — rename/move file
|
// POST /api/docs/files/rename — rename/move file
|
||||||
router.post('/files/rename', async (req, res, next) => {
|
router.post('/files/rename', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
metrics_1.cm_docs_operations.inc({ operation: 'rename' });
|
metrics_1.cm_docs_operations.inc({ operation: 'rename' });
|
||||||
const { from, to } = req.body;
|
const { from, to } = req.body;
|
||||||
@ -107,14 +218,16 @@ router.post('/files/rename', async (req, res, next) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await docs_files_service_1.docsFilesService.renameFile(from, to);
|
await docs_files_service_1.docsFilesService.renameFile(from, to);
|
||||||
|
// Invalidate old path's collaboration state
|
||||||
|
docs_collab_service_1.docsCollabService.invalidateDocument(from).catch(() => { });
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
handleFileError(err, res, next);
|
handleFileError(err, res, next);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// GET /api/docs/files/* — read file content
|
// GET /api/docs/files/* — read file content (content editors only)
|
||||||
router.get('/files/*', async (req, res, next) => {
|
router.get('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
metrics_1.cm_docs_operations.inc({ operation: 'read' });
|
metrics_1.cm_docs_operations.inc({ operation: 'read' });
|
||||||
const filePath = extractWildcardPath(req);
|
const filePath = extractWildcardPath(req);
|
||||||
@ -130,7 +243,7 @@ router.get('/files/*', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// PUT /api/docs/files/* — write/update file content
|
// PUT /api/docs/files/* — write/update file content
|
||||||
router.put('/files/*', async (req, res, next) => {
|
router.put('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
metrics_1.cm_docs_operations.inc({ operation: 'write' });
|
metrics_1.cm_docs_operations.inc({ operation: 'write' });
|
||||||
const filePath = extractWildcardPath(req);
|
const filePath = extractWildcardPath(req);
|
||||||
@ -144,6 +257,8 @@ router.put('/files/*', async (req, res, next) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await docs_files_service_1.docsFilesService.writeFileContent(filePath, content);
|
await docs_files_service_1.docsFilesService.writeFileContent(filePath, content);
|
||||||
|
// Invalidate collaboration state so next session starts fresh from disk
|
||||||
|
docs_collab_service_1.docsCollabService.invalidateDocument(filePath).catch(() => { });
|
||||||
res.json({ success: true, path: filePath });
|
res.json({ success: true, path: filePath });
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
@ -151,7 +266,7 @@ router.put('/files/*', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// POST /api/docs/files/* — create new file or folder
|
// POST /api/docs/files/* — create new file or folder
|
||||||
router.post('/files/*', async (req, res, next) => {
|
router.post('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
metrics_1.cm_docs_operations.inc({ operation: 'create' });
|
metrics_1.cm_docs_operations.inc({ operation: 'create' });
|
||||||
const filePath = extractWildcardPath(req);
|
const filePath = extractWildcardPath(req);
|
||||||
@ -168,7 +283,7 @@ router.post('/files/*', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// DELETE /api/docs/files/* — delete file or empty folder
|
// DELETE /api/docs/files/* — delete file or empty folder
|
||||||
router.delete('/files/*', async (req, res, next) => {
|
router.delete('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
metrics_1.cm_docs_operations.inc({ operation: 'delete' });
|
metrics_1.cm_docs_operations.inc({ operation: 'delete' });
|
||||||
const filePath = extractWildcardPath(req);
|
const filePath = extractWildcardPath(req);
|
||||||
@ -177,6 +292,8 @@ router.delete('/files/*', async (req, res, next) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await docs_files_service_1.docsFilesService.deleteFile(filePath);
|
await docs_files_service_1.docsFilesService.deleteFile(filePath);
|
||||||
|
// Invalidate collaboration state for deleted file
|
||||||
|
docs_collab_service_1.docsCollabService.invalidateDocument(filePath).catch(() => { });
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
|
|||||||
2
api/dist/modules/docs/docs.routes.js.map
vendored
2
api/dist/modules/docs/docs.routes.js.map
vendored
File diff suppressed because one or more lines are too long
@ -12,7 +12,8 @@ declare function validateYaml(content: string): string | null;
|
|||||||
declare function readConfig(): Promise<string>;
|
declare function readConfig(): Promise<string>;
|
||||||
declare function writeConfig(content: string): Promise<void>;
|
declare function writeConfig(content: string): Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Execute `mkdocs build` inside the running MkDocs container via Docker Engine API.
|
* Trigger `mkdocs build --clean` via the build trigger HTTP server
|
||||||
|
* running inside the MkDocs container on port 8001.
|
||||||
*/
|
*/
|
||||||
declare function triggerBuild(): Promise<{
|
declare function triggerBuild(): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"mkdocs-config.service.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/mkdocs-config.service.ts"],"names":[],"mappings":"AAIA,OAAO,EAAiB,QAAQ,EAAE,MAAM,MAAM,CAAC;AAuB/C;;;GAGG;AACH,iBAAS,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAS9C;AAED;;;GAGG;AACH,iBAAS,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWpD;AAED,iBAAe,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAE3C;AAED,iBAAe,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAezD;AA8DD;;GAEG;AACH,iBAAe,YAAY,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAgE7F;AAED,eAAO,MAAM,mBAAmB;;;;;;CAM/B,CAAC"}
|
{"version":3,"file":"mkdocs-config.service.d.ts","sourceRoot":"","sources":["../../../src/modules/docs/mkdocs-config.service.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,QAAQ,EAAE,MAAM,MAAM,CAAC;AAqB/C;;;GAGG;AACH,iBAAS,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAS9C;AAED;;;GAGG;AACH,iBAAS,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWpD;AAED,iBAAe,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAE3C;AAED,iBAAe,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAezD;AAED;;;GAGG;AACH,iBAAe,YAAY,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAyB7F;AAED,eAAO,MAAM,mBAAmB;;;;;;CAM/B,CAAC"}
|
||||||
104
api/dist/modules/docs/mkdocs-config.service.js
vendored
104
api/dist/modules/docs/mkdocs-config.service.js
vendored
@ -2,11 +2,9 @@
|
|||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.mkdocsConfigService = void 0;
|
exports.mkdocsConfigService = void 0;
|
||||||
const promises_1 = require("fs/promises");
|
const promises_1 = require("fs/promises");
|
||||||
const http_1 = require("http");
|
|
||||||
const env_1 = require("../../config/env");
|
const env_1 = require("../../config/env");
|
||||||
const logger_1 = require("../../utils/logger");
|
const logger_1 = require("../../utils/logger");
|
||||||
const yaml_1 = require("yaml");
|
const yaml_1 = require("yaml");
|
||||||
const DOCKER_SOCKET = '/var/run/docker.sock';
|
|
||||||
/**
|
/**
|
||||||
* Custom YAML tag schema to preserve !!python/name: and !!python/object: tags.
|
* Custom YAML tag schema to preserve !!python/name: and !!python/object: tags.
|
||||||
* Without this, the yaml library would reject these custom tags.
|
* Without this, the yaml library would reject these custom tags.
|
||||||
@ -73,102 +71,22 @@ async function writeConfig(content) {
|
|||||||
await (0, promises_1.writeFile)(env_1.env.MKDOCS_CONFIG_PATH, content, 'utf-8');
|
await (0, promises_1.writeFile)(env_1.env.MKDOCS_CONFIG_PATH, content, 'utf-8');
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Make a request to the Docker Engine API over Unix socket.
|
* Trigger `mkdocs build --clean` via the build trigger HTTP server
|
||||||
*/
|
* running inside the MkDocs container on port 8001.
|
||||||
function dockerRequest(method, path, body) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const options = {
|
|
||||||
socketPath: DOCKER_SOCKET,
|
|
||||||
path,
|
|
||||||
method,
|
|
||||||
headers: body
|
|
||||||
? { 'Content-Type': 'application/json' }
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
const req = (0, http_1.request)(options, (res) => {
|
|
||||||
const chunks = [];
|
|
||||||
res.on('data', (chunk) => chunks.push(chunk));
|
|
||||||
res.on('end', () => {
|
|
||||||
resolve({
|
|
||||||
statusCode: res.statusCode || 0,
|
|
||||||
body: Buffer.concat(chunks).toString(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
req.on('error', reject);
|
|
||||||
if (body) {
|
|
||||||
req.write(JSON.stringify(body));
|
|
||||||
}
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read raw output from a Docker exec start stream.
|
|
||||||
* Docker multiplexes stdout/stderr with 8-byte headers.
|
|
||||||
*/
|
|
||||||
function demuxDockerStream(raw) {
|
|
||||||
const lines = [];
|
|
||||||
let offset = 0;
|
|
||||||
while (offset < raw.length) {
|
|
||||||
if (offset + 8 > raw.length)
|
|
||||||
break;
|
|
||||||
// byte 0: stream type (1=stdout, 2=stderr)
|
|
||||||
const size = raw.readUInt32BE(offset + 4);
|
|
||||||
offset += 8;
|
|
||||||
if (offset + size > raw.length) {
|
|
||||||
lines.push(raw.subarray(offset).toString('utf-8'));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
lines.push(raw.subarray(offset, offset + size).toString('utf-8'));
|
|
||||||
offset += size;
|
|
||||||
}
|
|
||||||
return lines.join('');
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Execute `mkdocs build` inside the running MkDocs container via Docker Engine API.
|
|
||||||
*/
|
*/
|
||||||
async function triggerBuild() {
|
async function triggerBuild() {
|
||||||
const containerName = env_1.env.MKDOCS_CONTAINER_NAME;
|
const buildUrl = `${env_1.env.MKDOCS_PREVIEW_URL.replace(':8000', ':8001')}/build`;
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
try {
|
try {
|
||||||
// 1. Create exec instance
|
const controller = new AbortController();
|
||||||
const execCreate = await dockerRequest('POST', `/containers/${containerName}/exec`, {
|
const timeout = setTimeout(() => controller.abort(), 130_000);
|
||||||
AttachStdout: true,
|
const response = await fetch(buildUrl, {
|
||||||
AttachStderr: true,
|
method: 'POST',
|
||||||
Cmd: ['mkdocs', 'build', '--clean'],
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
if (execCreate.statusCode !== 201) {
|
clearTimeout(timeout);
|
||||||
throw new Error(`Failed to create exec: ${execCreate.body}`);
|
const data = await response.json();
|
||||||
}
|
return data;
|
||||||
const { Id: execId } = JSON.parse(execCreate.body);
|
|
||||||
// 2. Start exec and collect output
|
|
||||||
const execOutput = await new Promise((resolve, reject) => {
|
|
||||||
const options = {
|
|
||||||
socketPath: DOCKER_SOCKET,
|
|
||||||
path: `/exec/${execId}/start`,
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
};
|
|
||||||
const req = (0, http_1.request)(options, (res) => {
|
|
||||||
const chunks = [];
|
|
||||||
res.on('data', (chunk) => chunks.push(chunk));
|
|
||||||
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
||||||
});
|
|
||||||
req.on('error', reject);
|
|
||||||
req.write(JSON.stringify({ Detach: false, Tty: false }));
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
const output = demuxDockerStream(execOutput);
|
|
||||||
// 3. Check exit code
|
|
||||||
const execInspect = await dockerRequest('GET', `/exec/${execId}/json`);
|
|
||||||
const inspectData = JSON.parse(execInspect.body);
|
|
||||||
const exitCode = inspectData.ExitCode ?? -1;
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
return {
|
|
||||||
success: exitCode === 0,
|
|
||||||
output: output || '(no output)',
|
|
||||||
duration,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
{"version":3,"file":"email-templates-admin.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/email-templates/email-templates-admin.routes.ts"],"names":[],"mappings":"AAqBA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA8UxB,eAAe,MAAM,CAAC"}
|
{"version":3,"file":"email-templates-admin.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/email-templates/email-templates-admin.routes.ts"],"names":[],"mappings":"AAuBA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAqUxB,eAAe,MAAM,CAAC"}
|
||||||
@ -12,19 +12,21 @@ const logger_1 = require("../../utils/logger");
|
|||||||
const auth_middleware_1 = require("../../middleware/auth.middleware");
|
const auth_middleware_1 = require("../../middleware/auth.middleware");
|
||||||
const rbac_middleware_1 = require("../../middleware/rbac.middleware");
|
const rbac_middleware_1 = require("../../middleware/rbac.middleware");
|
||||||
const client_1 = require("@prisma/client");
|
const client_1 = require("@prisma/client");
|
||||||
|
const roles_1 = require("../../utils/roles");
|
||||||
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
|
||||||
const rate_limit_redis_1 = __importDefault(require("rate-limit-redis"));
|
const rate_limit_redis_1 = __importDefault(require("rate-limit-redis"));
|
||||||
const redis_1 = require("../../config/redis");
|
const redis_1 = require("../../config/redis");
|
||||||
|
const seed_email_templates_1 = require("../../scripts/seed-email-templates");
|
||||||
const router = (0, express_1.Router)();
|
const router = (0, express_1.Router)();
|
||||||
// All email template routes require authentication
|
// All email template routes require authentication
|
||||||
router.use(auth_middleware_1.authenticate);
|
router.use(auth_middleware_1.authenticate);
|
||||||
// All routes require admin role (SUPER_ADMIN, INFLUENCE_ADMIN, or MAP_ADMIN)
|
// All routes require broadcast admin role
|
||||||
const requireAdminRole = (0, rbac_middleware_1.requireRole)(client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN);
|
const requireBroadcastRole = (0, rbac_middleware_1.requireRole)(...roles_1.BROADCAST_ROLES);
|
||||||
/**
|
/**
|
||||||
* List email templates
|
* List email templates
|
||||||
* GET /email-templates
|
* GET /email-templates
|
||||||
*/
|
*/
|
||||||
router.get('/', requireAdminRole, (0, validate_1.validate)(email_templates_schemas_1.listEmailTemplatesSchema, 'query'), async (req, res) => {
|
router.get('/', requireBroadcastRole, (0, validate_1.validate)(email_templates_schemas_1.listEmailTemplatesSchema, 'query'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await email_templates_service_1.emailTemplatesService.list(req.query);
|
const result = await email_templates_service_1.emailTemplatesService.list(req.query);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
@ -38,7 +40,7 @@ router.get('/', requireAdminRole, (0, validate_1.validate)(email_templates_schem
|
|||||||
* Get single email template
|
* Get single email template
|
||||||
* GET /email-templates/:id
|
* GET /email-templates/:id
|
||||||
*/
|
*/
|
||||||
router.get('/:id', requireAdminRole, async (req, res) => {
|
router.get('/:id', requireBroadcastRole, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const template = await email_templates_service_1.emailTemplatesService.getById(req.params.id);
|
const template = await email_templates_service_1.emailTemplatesService.getById(req.params.id);
|
||||||
res.json(template);
|
res.json(template);
|
||||||
@ -56,7 +58,7 @@ router.get('/:id', requireAdminRole, async (req, res) => {
|
|||||||
* Create email template
|
* Create email template
|
||||||
* POST /email-templates
|
* POST /email-templates
|
||||||
*/
|
*/
|
||||||
router.post('/', requireAdminRole, (0, validate_1.validate)(email_templates_schemas_1.createEmailTemplateSchema), async (req, res) => {
|
router.post('/', requireBroadcastRole, (0, validate_1.validate)(email_templates_schemas_1.createEmailTemplateSchema), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const template = await email_templates_service_1.emailTemplatesService.create(req.body, req.user.id);
|
const template = await email_templates_service_1.emailTemplatesService.create(req.body, req.user.id);
|
||||||
res.status(201).json(template);
|
res.status(201).json(template);
|
||||||
@ -78,7 +80,7 @@ router.post('/', requireAdminRole, (0, validate_1.validate)(email_templates_sche
|
|||||||
* Update email template
|
* Update email template
|
||||||
* PUT /email-templates/:id
|
* PUT /email-templates/:id
|
||||||
*/
|
*/
|
||||||
router.put('/:id', requireAdminRole, (0, validate_1.validate)(email_templates_schemas_1.updateEmailTemplateSchema), async (req, res) => {
|
router.put('/:id', requireBroadcastRole, (0, validate_1.validate)(email_templates_schemas_1.updateEmailTemplateSchema), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const template = await email_templates_service_1.emailTemplatesService.update(req.params.id, req.body, req.user.id);
|
const template = await email_templates_service_1.emailTemplatesService.update(req.params.id, req.body, req.user.id);
|
||||||
// Clear cache so changes take effect immediately
|
// Clear cache so changes take effect immediately
|
||||||
@ -103,7 +105,7 @@ router.put('/:id', requireAdminRole, (0, validate_1.validate)(email_templates_sc
|
|||||||
* Delete email template
|
* Delete email template
|
||||||
* DELETE /email-templates/:id
|
* DELETE /email-templates/:id
|
||||||
*/
|
*/
|
||||||
router.delete('/:id', requireAdminRole, async (req, res) => {
|
router.delete('/:id', requireBroadcastRole, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Fetch template before deleting to get the key
|
// Fetch template before deleting to get the key
|
||||||
const template = await email_templates_service_1.emailTemplatesService.getById(req.params.id);
|
const template = await email_templates_service_1.emailTemplatesService.getById(req.params.id);
|
||||||
@ -130,7 +132,7 @@ router.delete('/:id', requireAdminRole, async (req, res) => {
|
|||||||
* Get version history
|
* Get version history
|
||||||
* GET /email-templates/:id/versions
|
* GET /email-templates/:id/versions
|
||||||
*/
|
*/
|
||||||
router.get('/:id/versions', requireAdminRole, async (req, res) => {
|
router.get('/:id/versions', requireBroadcastRole, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const versions = await email_templates_service_1.emailTemplatesService.getVersions(req.params.id);
|
const versions = await email_templates_service_1.emailTemplatesService.getVersions(req.params.id);
|
||||||
res.json(versions);
|
res.json(versions);
|
||||||
@ -144,7 +146,7 @@ router.get('/:id/versions', requireAdminRole, async (req, res) => {
|
|||||||
* Get specific version
|
* Get specific version
|
||||||
* GET /email-templates/:id/versions/:versionNumber
|
* GET /email-templates/:id/versions/:versionNumber
|
||||||
*/
|
*/
|
||||||
router.get('/:id/versions/:versionNumber', requireAdminRole, async (req, res) => {
|
router.get('/:id/versions/:versionNumber', requireBroadcastRole, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const version = await email_templates_service_1.emailTemplatesService.getVersion(req.params.id, parseInt(req.params.versionNumber, 10));
|
const version = await email_templates_service_1.emailTemplatesService.getVersion(req.params.id, parseInt(req.params.versionNumber, 10));
|
||||||
res.json(version);
|
res.json(version);
|
||||||
@ -162,7 +164,7 @@ router.get('/:id/versions/:versionNumber', requireAdminRole, async (req, res) =>
|
|||||||
* Rollback to previous version
|
* Rollback to previous version
|
||||||
* POST /email-templates/:id/rollback
|
* POST /email-templates/:id/rollback
|
||||||
*/
|
*/
|
||||||
router.post('/:id/rollback', requireAdminRole, (0, validate_1.validate)(email_templates_schemas_1.rollbackToVersionSchema), async (req, res) => {
|
router.post('/:id/rollback', requireBroadcastRole, (0, validate_1.validate)(email_templates_schemas_1.rollbackToVersionSchema), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const template = await email_templates_service_1.emailTemplatesService.rollbackToVersion(req.params.id, req.body, req.user.id);
|
const template = await email_templates_service_1.emailTemplatesService.rollbackToVersion(req.params.id, req.body, req.user.id);
|
||||||
res.json(template);
|
res.json(template);
|
||||||
@ -180,7 +182,7 @@ router.post('/:id/rollback', requireAdminRole, (0, validate_1.validate)(email_te
|
|||||||
* Validate template syntax
|
* Validate template syntax
|
||||||
* POST /email-templates/validate
|
* POST /email-templates/validate
|
||||||
*/
|
*/
|
||||||
router.post('/validate', requireAdminRole, (0, validate_1.validate)(email_templates_schemas_1.validateTemplateSchema), async (req, res) => {
|
router.post('/validate', requireBroadcastRole, (0, validate_1.validate)(email_templates_schemas_1.validateTemplateSchema), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = email_templates_service_1.emailTemplatesService.validateTemplate(req.body);
|
const result = email_templates_service_1.emailTemplatesService.validateTemplate(req.body);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
@ -195,7 +197,7 @@ router.post('/validate', requireAdminRole, (0, validate_1.validate)(email_templa
|
|||||||
* POST /email-templates/:id/test
|
* POST /email-templates/:id/test
|
||||||
* Rate limited to 10 per 15 minutes per user
|
* Rate limited to 10 per 15 minutes per user
|
||||||
*/
|
*/
|
||||||
router.post('/:id/test', requireAdminRole, (0, express_rate_limit_1.default)({
|
router.post('/:id/test', requireBroadcastRole, (0, express_rate_limit_1.default)({
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
max: 10,
|
max: 10,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
@ -223,7 +225,7 @@ router.post('/:id/test', requireAdminRole, (0, express_rate_limit_1.default)({
|
|||||||
* Get test logs for template
|
* Get test logs for template
|
||||||
* GET /email-templates/:id/test-logs
|
* GET /email-templates/:id/test-logs
|
||||||
*/
|
*/
|
||||||
router.get('/:id/test-logs', requireAdminRole, async (req, res) => {
|
router.get('/:id/test-logs', requireBroadcastRole, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 10;
|
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 10;
|
||||||
const logs = await email_templates_service_1.emailTemplatesService.getTestLogs(req.params.id, limit);
|
const logs = await email_templates_service_1.emailTemplatesService.getTestLogs(req.params.id, limit);
|
||||||
@ -240,16 +242,9 @@ router.get('/:id/test-logs', requireAdminRole, async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.post('/seed', (0, rbac_middleware_1.requireRole)(client_1.UserRole.SUPER_ADMIN), async (req, res) => {
|
router.post('/seed', (0, rbac_middleware_1.requireRole)(client_1.UserRole.SUPER_ADMIN), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// This is a placeholder - the actual seeding is done via the script
|
await (0, seed_email_templates_1.seedEmailTemplates)();
|
||||||
// But we keep this endpoint for manual triggering if needed
|
|
||||||
const { exec } = require('child_process');
|
|
||||||
const { promisify } = require('util');
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
const result = await execAsync('npx tsx src/scripts/seed-email-templates.ts', {
|
|
||||||
cwd: '/app',
|
|
||||||
});
|
|
||||||
logger_1.logger.info('Email templates seeded via API');
|
logger_1.logger.info('Email templates seeded via API');
|
||||||
res.json({ success: true, output: result.stdout });
|
res.json({ success: true, message: 'Templates seeded successfully' });
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
logger_1.logger.error('Error seeding templates:', error);
|
logger_1.logger.error('Error seeding templates:', error);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -14,8 +14,8 @@ export declare const emailTemplateVariableSchema: z.ZodEffects<z.ZodObject<{
|
|||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
type: "VIDEO" | "TEXT";
|
type: "VIDEO" | "TEXT";
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
label: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
isConditional: boolean;
|
isConditional: boolean;
|
||||||
videoId?: number | undefined;
|
videoId?: number | undefined;
|
||||||
@ -34,8 +34,8 @@ export declare const emailTemplateVariableSchema: z.ZodEffects<z.ZodObject<{
|
|||||||
}>, {
|
}>, {
|
||||||
type: "VIDEO" | "TEXT";
|
type: "VIDEO" | "TEXT";
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
label: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
isConditional: boolean;
|
isConditional: boolean;
|
||||||
videoId?: number | undefined;
|
videoId?: number | undefined;
|
||||||
@ -60,20 +60,21 @@ export declare const listEmailTemplatesSchema: z.ZodObject<{
|
|||||||
INFLUENCE: "INFLUENCE";
|
INFLUENCE: "INFLUENCE";
|
||||||
MAP: "MAP";
|
MAP: "MAP";
|
||||||
SYSTEM: "SYSTEM";
|
SYSTEM: "SYSTEM";
|
||||||
|
PAYMENT: "PAYMENT";
|
||||||
}>>;
|
}>>;
|
||||||
isActive: z.ZodOptional<z.ZodBoolean>;
|
isActive: z.ZodOptional<z.ZodBoolean>;
|
||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
limit: number;
|
limit: number;
|
||||||
page: number;
|
page: number;
|
||||||
search?: string | undefined;
|
search?: string | undefined;
|
||||||
category?: "INFLUENCE" | "MAP" | "SYSTEM" | undefined;
|
category?: "INFLUENCE" | "MAP" | "SYSTEM" | "PAYMENT" | undefined;
|
||||||
isActive?: boolean | undefined;
|
isActive?: boolean | undefined;
|
||||||
}, {
|
}, {
|
||||||
search?: string | undefined;
|
search?: string | undefined;
|
||||||
|
category?: "INFLUENCE" | "MAP" | "SYSTEM" | "PAYMENT" | undefined;
|
||||||
limit?: number | undefined;
|
limit?: number | undefined;
|
||||||
category?: "INFLUENCE" | "MAP" | "SYSTEM" | undefined;
|
|
||||||
page?: number | undefined;
|
|
||||||
isActive?: boolean | undefined;
|
isActive?: boolean | undefined;
|
||||||
|
page?: number | undefined;
|
||||||
}>;
|
}>;
|
||||||
export type ListEmailTemplatesDto = z.infer<typeof listEmailTemplatesSchema>;
|
export type ListEmailTemplatesDto = z.infer<typeof listEmailTemplatesSchema>;
|
||||||
export declare const createEmailTemplateSchema: z.ZodObject<{
|
export declare const createEmailTemplateSchema: z.ZodObject<{
|
||||||
@ -84,6 +85,7 @@ export declare const createEmailTemplateSchema: z.ZodObject<{
|
|||||||
INFLUENCE: "INFLUENCE";
|
INFLUENCE: "INFLUENCE";
|
||||||
MAP: "MAP";
|
MAP: "MAP";
|
||||||
SYSTEM: "SYSTEM";
|
SYSTEM: "SYSTEM";
|
||||||
|
PAYMENT: "PAYMENT";
|
||||||
}>;
|
}>;
|
||||||
subjectLine: z.ZodString;
|
subjectLine: z.ZodString;
|
||||||
htmlContent: z.ZodString;
|
htmlContent: z.ZodString;
|
||||||
@ -102,8 +104,8 @@ export declare const createEmailTemplateSchema: z.ZodObject<{
|
|||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
type: "VIDEO" | "TEXT";
|
type: "VIDEO" | "TEXT";
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
label: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
isConditional: boolean;
|
isConditional: boolean;
|
||||||
videoId?: number | undefined;
|
videoId?: number | undefined;
|
||||||
@ -122,8 +124,8 @@ export declare const createEmailTemplateSchema: z.ZodObject<{
|
|||||||
}>, {
|
}>, {
|
||||||
type: "VIDEO" | "TEXT";
|
type: "VIDEO" | "TEXT";
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
label: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
isConditional: boolean;
|
isConditional: boolean;
|
||||||
videoId?: number | undefined;
|
videoId?: number | undefined;
|
||||||
@ -142,9 +144,9 @@ export declare const createEmailTemplateSchema: z.ZodObject<{
|
|||||||
}>, "many">>;
|
}>, "many">>;
|
||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
name: string;
|
name: string;
|
||||||
category: "INFLUENCE" | "MAP" | "SYSTEM";
|
category: "INFLUENCE" | "MAP" | "SYSTEM" | "PAYMENT";
|
||||||
isActive: boolean;
|
|
||||||
key: string;
|
key: string;
|
||||||
|
isActive: boolean;
|
||||||
subjectLine: string;
|
subjectLine: string;
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
textContent: string;
|
textContent: string;
|
||||||
@ -152,8 +154,8 @@ export declare const createEmailTemplateSchema: z.ZodObject<{
|
|||||||
variables?: {
|
variables?: {
|
||||||
type: "VIDEO" | "TEXT";
|
type: "VIDEO" | "TEXT";
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
label: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
isConditional: boolean;
|
isConditional: boolean;
|
||||||
videoId?: number | undefined;
|
videoId?: number | undefined;
|
||||||
@ -162,13 +164,13 @@ export declare const createEmailTemplateSchema: z.ZodObject<{
|
|||||||
}[] | undefined;
|
}[] | undefined;
|
||||||
}, {
|
}, {
|
||||||
name: string;
|
name: string;
|
||||||
category: "INFLUENCE" | "MAP" | "SYSTEM";
|
category: "INFLUENCE" | "MAP" | "SYSTEM" | "PAYMENT";
|
||||||
key: string;
|
key: string;
|
||||||
subjectLine: string;
|
subjectLine: string;
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
textContent: string;
|
textContent: string;
|
||||||
description?: string | undefined;
|
|
||||||
isActive?: boolean | undefined;
|
isActive?: boolean | undefined;
|
||||||
|
description?: string | undefined;
|
||||||
variables?: {
|
variables?: {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
@ -189,6 +191,7 @@ export declare const updateEmailTemplateSchema: z.ZodObject<{
|
|||||||
INFLUENCE: "INFLUENCE";
|
INFLUENCE: "INFLUENCE";
|
||||||
MAP: "MAP";
|
MAP: "MAP";
|
||||||
SYSTEM: "SYSTEM";
|
SYSTEM: "SYSTEM";
|
||||||
|
PAYMENT: "PAYMENT";
|
||||||
}>>;
|
}>>;
|
||||||
subjectLine: z.ZodOptional<z.ZodString>;
|
subjectLine: z.ZodOptional<z.ZodString>;
|
||||||
htmlContent: z.ZodOptional<z.ZodString>;
|
htmlContent: z.ZodOptional<z.ZodString>;
|
||||||
@ -207,8 +210,8 @@ export declare const updateEmailTemplateSchema: z.ZodObject<{
|
|||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
type: "VIDEO" | "TEXT";
|
type: "VIDEO" | "TEXT";
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
label: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
isConditional: boolean;
|
isConditional: boolean;
|
||||||
videoId?: number | undefined;
|
videoId?: number | undefined;
|
||||||
@ -227,8 +230,8 @@ export declare const updateEmailTemplateSchema: z.ZodObject<{
|
|||||||
}>, {
|
}>, {
|
||||||
type: "VIDEO" | "TEXT";
|
type: "VIDEO" | "TEXT";
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
label: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
isConditional: boolean;
|
isConditional: boolean;
|
||||||
videoId?: number | undefined;
|
videoId?: number | undefined;
|
||||||
@ -247,17 +250,17 @@ export declare const updateEmailTemplateSchema: z.ZodObject<{
|
|||||||
}>, "many">>;
|
}>, "many">>;
|
||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
name?: string | undefined;
|
name?: string | undefined;
|
||||||
category?: "INFLUENCE" | "MAP" | "SYSTEM" | undefined;
|
category?: "INFLUENCE" | "MAP" | "SYSTEM" | "PAYMENT" | undefined;
|
||||||
description?: string | null | undefined;
|
|
||||||
isActive?: boolean | undefined;
|
isActive?: boolean | undefined;
|
||||||
|
description?: string | null | undefined;
|
||||||
subjectLine?: string | undefined;
|
subjectLine?: string | undefined;
|
||||||
htmlContent?: string | undefined;
|
htmlContent?: string | undefined;
|
||||||
textContent?: string | undefined;
|
textContent?: string | undefined;
|
||||||
variables?: {
|
variables?: {
|
||||||
type: "VIDEO" | "TEXT";
|
type: "VIDEO" | "TEXT";
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
label: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
isConditional: boolean;
|
isConditional: boolean;
|
||||||
videoId?: number | undefined;
|
videoId?: number | undefined;
|
||||||
@ -266,9 +269,9 @@ export declare const updateEmailTemplateSchema: z.ZodObject<{
|
|||||||
}[] | undefined;
|
}[] | undefined;
|
||||||
}, {
|
}, {
|
||||||
name?: string | undefined;
|
name?: string | undefined;
|
||||||
category?: "INFLUENCE" | "MAP" | "SYSTEM" | undefined;
|
category?: "INFLUENCE" | "MAP" | "SYSTEM" | "PAYMENT" | undefined;
|
||||||
description?: string | null | undefined;
|
|
||||||
isActive?: boolean | undefined;
|
isActive?: boolean | undefined;
|
||||||
|
description?: string | null | undefined;
|
||||||
subjectLine?: string | undefined;
|
subjectLine?: string | undefined;
|
||||||
htmlContent?: string | undefined;
|
htmlContent?: string | undefined;
|
||||||
textContent?: string | undefined;
|
textContent?: string | undefined;
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"email-templates.schemas.d.ts","sourceRoot":"","sources":["../../../src/modules/email-templates/email-templates.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,+BAA+B,8BAA4B,CAAC;AACzE,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAG5F,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmBvC,CAAC;AAGF,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;EAMnC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAG7E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUpC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAG/E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASpC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAG/E,eAAO,MAAM,uBAAuB;;;;;;;;;EAGlC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAG3E,eAAO,MAAM,sBAAsB;;;;;;;;;;;;EAIjC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAGzE,eAAO,MAAM,mBAAmB;;;;;;;;;EAG9B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
{"version":3,"file":"email-templates.schemas.d.ts","sourceRoot":"","sources":["../../../src/modules/email-templates/email-templates.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,+BAA+B,8BAA4B,CAAC;AACzE,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAG5F,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmBvC,CAAC;AAGF,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;EAMnC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAG7E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUpC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAG/E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASpC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAG/E,eAAO,MAAM,uBAAuB;;;;;;;;;EAGlC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAG3E,eAAO,MAAM,sBAAsB;;;;;;;;;;;;EAIjC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAGzE,eAAO,MAAM,mBAAmB;;;;;;;;;EAG9B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
||||||
@ -28,15 +28,20 @@ export declare class EmailTemplatesService {
|
|||||||
versions: number;
|
versions: number;
|
||||||
testLogs: number;
|
testLogs: number;
|
||||||
};
|
};
|
||||||
|
createdBy: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string | null;
|
||||||
|
};
|
||||||
variables: {
|
variables: {
|
||||||
type: import(".prisma/client").$Enums.EmailTemplateVariableType;
|
type: import(".prisma/client").$Enums.EmailTemplateVariableType;
|
||||||
id: string;
|
id: string;
|
||||||
videoId: number | null;
|
videoId: number | null;
|
||||||
description: string | null;
|
|
||||||
key: string;
|
key: string;
|
||||||
templateId: string;
|
description: string | null;
|
||||||
label: string;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
label: string;
|
||||||
|
templateId: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
isConditional: boolean;
|
isConditional: boolean;
|
||||||
sampleValue: string | null;
|
sampleValue: string | null;
|
||||||
@ -46,26 +51,21 @@ export declare class EmailTemplatesService {
|
|||||||
email: string;
|
email: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
createdBy: {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
name: string | null;
|
|
||||||
};
|
|
||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
||||||
description: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
key: string;
|
key: string;
|
||||||
createdByUserId: string;
|
isActive: boolean;
|
||||||
updatedByUserId: string | null;
|
description: string | null;
|
||||||
subjectLine: string;
|
subjectLine: string;
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
textContent: string;
|
textContent: string;
|
||||||
isSystem: boolean;
|
isSystem: boolean;
|
||||||
|
createdByUserId: string;
|
||||||
|
updatedByUserId: string | null;
|
||||||
}>;
|
}>;
|
||||||
/**
|
/**
|
||||||
* Get template by key
|
* Get template by key
|
||||||
@ -75,11 +75,11 @@ export declare class EmailTemplatesService {
|
|||||||
type: import(".prisma/client").$Enums.EmailTemplateVariableType;
|
type: import(".prisma/client").$Enums.EmailTemplateVariableType;
|
||||||
id: string;
|
id: string;
|
||||||
videoId: number | null;
|
videoId: number | null;
|
||||||
description: string | null;
|
|
||||||
key: string;
|
key: string;
|
||||||
templateId: string;
|
description: string | null;
|
||||||
label: string;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
label: string;
|
||||||
|
templateId: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
isConditional: boolean;
|
isConditional: boolean;
|
||||||
sampleValue: string | null;
|
sampleValue: string | null;
|
||||||
@ -90,15 +90,15 @@ export declare class EmailTemplatesService {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
||||||
description: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
key: string;
|
key: string;
|
||||||
createdByUserId: string;
|
isActive: boolean;
|
||||||
updatedByUserId: string | null;
|
description: string | null;
|
||||||
subjectLine: string;
|
subjectLine: string;
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
textContent: string;
|
textContent: string;
|
||||||
isSystem: boolean;
|
isSystem: boolean;
|
||||||
|
createdByUserId: string;
|
||||||
|
updatedByUserId: string | null;
|
||||||
}) | null>;
|
}) | null>;
|
||||||
/**
|
/**
|
||||||
* Create a new email template
|
* Create a new email template
|
||||||
@ -108,11 +108,11 @@ export declare class EmailTemplatesService {
|
|||||||
type: import(".prisma/client").$Enums.EmailTemplateVariableType;
|
type: import(".prisma/client").$Enums.EmailTemplateVariableType;
|
||||||
id: string;
|
id: string;
|
||||||
videoId: number | null;
|
videoId: number | null;
|
||||||
description: string | null;
|
|
||||||
key: string;
|
key: string;
|
||||||
templateId: string;
|
description: string | null;
|
||||||
label: string;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
label: string;
|
||||||
|
templateId: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
isConditional: boolean;
|
isConditional: boolean;
|
||||||
sampleValue: string | null;
|
sampleValue: string | null;
|
||||||
@ -123,15 +123,15 @@ export declare class EmailTemplatesService {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
||||||
description: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
key: string;
|
key: string;
|
||||||
createdByUserId: string;
|
isActive: boolean;
|
||||||
updatedByUserId: string | null;
|
description: string | null;
|
||||||
subjectLine: string;
|
subjectLine: string;
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
textContent: string;
|
textContent: string;
|
||||||
isSystem: boolean;
|
isSystem: boolean;
|
||||||
|
createdByUserId: string;
|
||||||
|
updatedByUserId: string | null;
|
||||||
}>;
|
}>;
|
||||||
/**
|
/**
|
||||||
* Update an email template
|
* Update an email template
|
||||||
@ -141,11 +141,11 @@ export declare class EmailTemplatesService {
|
|||||||
type: import(".prisma/client").$Enums.EmailTemplateVariableType;
|
type: import(".prisma/client").$Enums.EmailTemplateVariableType;
|
||||||
id: string;
|
id: string;
|
||||||
videoId: number | null;
|
videoId: number | null;
|
||||||
description: string | null;
|
|
||||||
key: string;
|
key: string;
|
||||||
templateId: string;
|
description: string | null;
|
||||||
label: string;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
label: string;
|
||||||
|
templateId: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
isConditional: boolean;
|
isConditional: boolean;
|
||||||
sampleValue: string | null;
|
sampleValue: string | null;
|
||||||
@ -156,15 +156,15 @@ export declare class EmailTemplatesService {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
||||||
description: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
key: string;
|
key: string;
|
||||||
createdByUserId: string;
|
isActive: boolean;
|
||||||
updatedByUserId: string | null;
|
description: string | null;
|
||||||
subjectLine: string;
|
subjectLine: string;
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
textContent: string;
|
textContent: string;
|
||||||
isSystem: boolean;
|
isSystem: boolean;
|
||||||
|
createdByUserId: string;
|
||||||
|
updatedByUserId: string | null;
|
||||||
}>;
|
}>;
|
||||||
/**
|
/**
|
||||||
* Delete an email template
|
* Delete an email template
|
||||||
@ -182,10 +182,10 @@ export declare class EmailTemplatesService {
|
|||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
createdByUserId: string;
|
|
||||||
subjectLine: string;
|
subjectLine: string;
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
textContent: string;
|
textContent: string;
|
||||||
|
createdByUserId: string;
|
||||||
versionNumber: number;
|
versionNumber: number;
|
||||||
changeNotes: string | null;
|
changeNotes: string | null;
|
||||||
templateId: string;
|
templateId: string;
|
||||||
@ -202,10 +202,10 @@ export declare class EmailTemplatesService {
|
|||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
createdByUserId: string;
|
|
||||||
subjectLine: string;
|
subjectLine: string;
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
textContent: string;
|
textContent: string;
|
||||||
|
createdByUserId: string;
|
||||||
versionNumber: number;
|
versionNumber: number;
|
||||||
changeNotes: string | null;
|
changeNotes: string | null;
|
||||||
templateId: string;
|
templateId: string;
|
||||||
@ -219,15 +219,15 @@ export declare class EmailTemplatesService {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
category: import(".prisma/client").$Enums.EmailTemplateCategory;
|
||||||
description: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
key: string;
|
key: string;
|
||||||
createdByUserId: string;
|
isActive: boolean;
|
||||||
updatedByUserId: string | null;
|
description: string | null;
|
||||||
subjectLine: string;
|
subjectLine: string;
|
||||||
htmlContent: string;
|
htmlContent: string;
|
||||||
textContent: string;
|
textContent: string;
|
||||||
isSystem: boolean;
|
isSystem: boolean;
|
||||||
|
createdByUserId: string;
|
||||||
|
updatedByUserId: string | null;
|
||||||
}>;
|
}>;
|
||||||
/**
|
/**
|
||||||
* Validate template syntax
|
* Validate template syntax
|
||||||
@ -252,12 +252,12 @@ export declare class EmailTemplatesService {
|
|||||||
} & {
|
} & {
|
||||||
id: string;
|
id: string;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
messageId: string | null;
|
||||||
recipientEmail: string;
|
recipientEmail: string;
|
||||||
sentAt: Date;
|
sentAt: Date;
|
||||||
templateId: string;
|
templateId: string;
|
||||||
testData: Prisma.JsonValue;
|
testData: Prisma.JsonValue;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
messageId: string | null;
|
|
||||||
sentByUserId: string;
|
sentByUserId: string;
|
||||||
})[]>;
|
})[]>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"campaign-emails.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.routes.ts"],"names":[],"mappings":"AAgBA,QAAA,MAAM,YAAY,4CAAW,CAAC;AAqC9B,QAAA,MAAM,WAAW,4CAAW,CAAC;AAiC7B,OAAO,EAAE,YAAY,IAAI,0BAA0B,EAAE,WAAW,IAAI,yBAAyB,EAAE,CAAC"}
|
{"version":3,"file":"campaign-emails.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.routes.ts"],"names":[],"mappings":"AAcA,QAAA,MAAM,YAAY,4CAAW,CAAC;AAqC9B,QAAA,MAAM,WAAW,4CAAW,CAAC;AAiC7B,OAAO,EAAE,YAAY,IAAI,0BAA0B,EAAE,WAAW,IAAI,yBAAyB,EAAE,CAAC"}
|
||||||
@ -2,14 +2,13 @@
|
|||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.campaignEmailsAdminRouter = exports.campaignEmailsPublicRouter = void 0;
|
exports.campaignEmailsAdminRouter = exports.campaignEmailsPublicRouter = void 0;
|
||||||
const express_1 = require("express");
|
const express_1 = require("express");
|
||||||
const client_1 = require("@prisma/client");
|
|
||||||
const campaign_emails_service_1 = require("./campaign-emails.service");
|
const campaign_emails_service_1 = require("./campaign-emails.service");
|
||||||
const campaign_emails_schemas_1 = require("./campaign-emails.schemas");
|
const campaign_emails_schemas_1 = require("./campaign-emails.schemas");
|
||||||
const validate_1 = require("../../../middleware/validate");
|
const validate_1 = require("../../../middleware/validate");
|
||||||
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
||||||
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
||||||
const rate_limit_1 = require("../../../middleware/rate-limit");
|
const rate_limit_1 = require("../../../middleware/rate-limit");
|
||||||
const ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
|
const roles_1 = require("../../../utils/roles");
|
||||||
// --- Public Routes (no auth) ---
|
// --- Public Routes (no auth) ---
|
||||||
const publicRouter = (0, express_1.Router)();
|
const publicRouter = (0, express_1.Router)();
|
||||||
exports.campaignEmailsPublicRouter = publicRouter;
|
exports.campaignEmailsPublicRouter = publicRouter;
|
||||||
@ -41,7 +40,7 @@ publicRouter.post('/:slug/track-mailto', rate_limit_1.emailRateLimit, (0, valida
|
|||||||
const adminRouter = (0, express_1.Router)();
|
const adminRouter = (0, express_1.Router)();
|
||||||
exports.campaignEmailsAdminRouter = adminRouter;
|
exports.campaignEmailsAdminRouter = adminRouter;
|
||||||
adminRouter.use(auth_middleware_1.authenticate);
|
adminRouter.use(auth_middleware_1.authenticate);
|
||||||
adminRouter.use((0, rbac_middleware_1.requireRole)(...ADMIN_ROLES));
|
adminRouter.use((0, rbac_middleware_1.requireRole)(...roles_1.INFLUENCE_ROLES));
|
||||||
// GET /api/campaigns/:id/emails
|
// GET /api/campaigns/:id/emails
|
||||||
adminRouter.get('/:id/emails', (0, validate_1.validate)(campaign_emails_schemas_1.listCampaignEmailsSchema, 'query'), async (req, res, next) => {
|
adminRouter.get('/:id/emails', (0, validate_1.validate)(campaign_emails_schemas_1.listCampaignEmailsSchema, 'query'), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"campaign-emails.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.routes.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"campaign-emails.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.routes.ts"],"names":[],"mappings":"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"}
|
||||||
@ -14,6 +14,7 @@ export declare const campaignEmailsService: {
|
|||||||
emails: {
|
emails: {
|
||||||
status: import(".prisma/client").$Enums.CampaignEmailStatus;
|
status: import(".prisma/client").$Enums.CampaignEmailStatus;
|
||||||
id: string;
|
id: string;
|
||||||
|
subject: string;
|
||||||
userEmail: string | null;
|
userEmail: string | null;
|
||||||
userName: string | null;
|
userName: string | null;
|
||||||
userPostalCode: string | null;
|
userPostalCode: string | null;
|
||||||
@ -21,7 +22,6 @@ export declare const campaignEmailsService: {
|
|||||||
recipientName: string | null;
|
recipientName: string | null;
|
||||||
recipientLevel: import(".prisma/client").$Enums.GovernmentLevel | null;
|
recipientLevel: import(".prisma/client").$Enums.GovernmentLevel | null;
|
||||||
emailMethod: import(".prisma/client").$Enums.EmailMethod;
|
emailMethod: import(".prisma/client").$Enums.EmailMethod;
|
||||||
subject: string;
|
|
||||||
sentAt: Date;
|
sentAt: Date;
|
||||||
}[];
|
}[];
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"campaign-emails.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.service.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAEnH,eAAO,MAAM,qBAAqB;oBACV,MAAM,QAAQ,sBAAsB,aAAa,MAAM;;;;;sBAwFrD,MAAM,QAAQ,gBAAgB,aAAa,MAAM;;;;;+BAgDxC,MAAM,WAAW,uBAAuB;;;;;;;;;;;;;;;;;;;;;yBA0C9C,MAAM;CAqClC,CAAC"}
|
{"version":3,"file":"campaign-emails.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaign-emails/campaign-emails.service.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAEnH,eAAO,MAAM,qBAAqB;oBACV,MAAM,QAAQ,sBAAsB,aAAa,MAAM;;;;;sBA+GrD,MAAM,QAAQ,gBAAgB,aAAa,MAAM;;;;;+BAwDxC,MAAM,WAAW,uBAAuB;;;;;;;;;;;;;;;;;;;;;yBA0C9C,MAAM;CAqClC,CAAC"}
|
||||||
@ -1,4 +1,37 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.campaignEmailsService = void 0;
|
exports.campaignEmailsService = void 0;
|
||||||
const client_1 = require("@prisma/client");
|
const client_1 = require("@prisma/client");
|
||||||
@ -6,6 +39,9 @@ const database_1 = require("../../../config/database");
|
|||||||
const error_handler_1 = require("../../../middleware/error-handler");
|
const error_handler_1 = require("../../../middleware/error-handler");
|
||||||
const email_queue_service_1 = require("../../../services/email-queue.service");
|
const email_queue_service_1 = require("../../../services/email-queue.service");
|
||||||
const metrics_1 = require("../../../utils/metrics");
|
const metrics_1 = require("../../../utils/metrics");
|
||||||
|
const crm_activity_1 = require("../../../utils/crm-activity");
|
||||||
|
const group_service_1 = require("../../social/group.service");
|
||||||
|
const achievements_service_1 = require("../../social/achievements.service");
|
||||||
exports.campaignEmailsService = {
|
exports.campaignEmailsService = {
|
||||||
async sendEmail(slug, data, senderIp) {
|
async sendEmail(slug, data, senderIp) {
|
||||||
const campaign = await database_1.prisma.campaign.findUnique({
|
const campaign = await database_1.prisma.campaign.findUnique({
|
||||||
@ -77,6 +113,26 @@ exports.campaignEmailsService = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
(0, metrics_1.recordCampaignEmail)(campaign.id);
|
(0, metrics_1.recordCampaignEmail)(campaign.id);
|
||||||
|
// CRM activity (fire-and-forget)
|
||||||
|
(0, crm_activity_1.recordCrmActivity)({
|
||||||
|
email: data.userEmail,
|
||||||
|
activityType: 'EMAIL_SENT',
|
||||||
|
title: `Sent campaign email: ${campaign.title}`,
|
||||||
|
metadata: { campaignId: campaign.id, campaignSlug: campaign.slug, recipientEmail: data.recipientEmail, emailMethod: data.emailMethod },
|
||||||
|
}).catch(() => { });
|
||||||
|
// Social group sync (fire-and-forget)
|
||||||
|
group_service_1.groupService.syncCampaignTeam(campaign.id).catch(() => { });
|
||||||
|
// Achievement check for registered users (fire-and-forget)
|
||||||
|
database_1.prisma.user.findUnique({ where: { email: data.userEmail }, select: { id: true } })
|
||||||
|
.then((user) => {
|
||||||
|
if (user)
|
||||||
|
achievements_service_1.achievementsService.checkAndUnlock(user.id, ['campaigns']).catch(() => { });
|
||||||
|
})
|
||||||
|
.catch(() => { });
|
||||||
|
// Fire-and-forget: check campaign milestones
|
||||||
|
Promise.resolve().then(() => __importStar(require('../../social/impact-stories.service'))).then(({ impactStoriesService }) => {
|
||||||
|
impactStoriesService.checkMilestones(campaign.id).catch(() => { });
|
||||||
|
}).catch(() => { });
|
||||||
return {
|
return {
|
||||||
id: campaignEmail.id,
|
id: campaignEmail.id,
|
||||||
status: campaignEmail.status,
|
status: campaignEmail.status,
|
||||||
@ -120,6 +176,12 @@ exports.campaignEmailsService = {
|
|||||||
senderIp,
|
senderIp,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Social group sync (fire-and-forget)
|
||||||
|
group_service_1.groupService.syncCampaignTeam(campaign.id).catch(() => { });
|
||||||
|
// Fire-and-forget: check campaign milestones
|
||||||
|
Promise.resolve().then(() => __importStar(require('../../social/impact-stories.service'))).then(({ impactStoriesService }) => {
|
||||||
|
impactStoriesService.checkMilestones(campaign.id).catch(() => { });
|
||||||
|
}).catch(() => { });
|
||||||
return {
|
return {
|
||||||
id: campaignEmail.id,
|
id: campaignEmail.id,
|
||||||
status: campaignEmail.status,
|
status: campaignEmail.status,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
{"version":3,"file":"campaigns-public.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns-public.routes.ts"],"names":[],"mappings":"AAGA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA6BxB,OAAO,EAAE,MAAM,IAAI,oBAAoB,EAAE,CAAC"}
|
{"version":3,"file":"campaigns-public.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns-public.routes.ts"],"names":[],"mappings":"AAKA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2HxB,OAAO,EAAE,MAAM,IAAI,oBAAoB,EAAE,CAAC"}
|
||||||
@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||||||
exports.campaignPublicRouter = void 0;
|
exports.campaignPublicRouter = void 0;
|
||||||
const express_1 = require("express");
|
const express_1 = require("express");
|
||||||
const campaigns_service_1 = require("./campaigns.service");
|
const campaigns_service_1 = require("./campaigns.service");
|
||||||
|
const database_1 = require("../../../config/database");
|
||||||
|
const redis_1 = require("../../../config/redis");
|
||||||
const router = (0, express_1.Router)();
|
const router = (0, express_1.Router)();
|
||||||
exports.campaignPublicRouter = router;
|
exports.campaignPublicRouter = router;
|
||||||
// GET /api/campaigns/public — list all active campaigns (public)
|
// GET /api/campaigns/public — list all active campaigns (public)
|
||||||
@ -26,4 +28,93 @@ router.get('/:slug/details', async (req, res, next) => {
|
|||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// GET /api/campaigns/:slug/related — related campaigns + upcoming shifts
|
||||||
|
router.get('/:slug/related', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const slug = req.params.slug;
|
||||||
|
const cacheKey = `campaign:related:${slug}`;
|
||||||
|
// Check cache
|
||||||
|
try {
|
||||||
|
const cached = await redis_1.redis.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
res.json(JSON.parse(cached));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* cache miss */ }
|
||||||
|
// Find current campaign
|
||||||
|
const campaign = await database_1.prisma.campaign.findFirst({
|
||||||
|
where: { slug, status: 'ACTIVE' },
|
||||||
|
select: { id: true, targetGovernmentLevels: true },
|
||||||
|
});
|
||||||
|
if (!campaign) {
|
||||||
|
res.json({ campaigns: [], shifts: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Related campaigns: same gov levels, exclude current, limit 3
|
||||||
|
const relatedCampaigns = await database_1.prisma.campaign.findMany({
|
||||||
|
where: {
|
||||||
|
status: 'ACTIVE',
|
||||||
|
id: { not: campaign.id },
|
||||||
|
...(campaign.targetGovernmentLevels.length > 0 && {
|
||||||
|
targetGovernmentLevels: { hasSome: campaign.targetGovernmentLevels },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
_count: { select: { emails: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 3,
|
||||||
|
});
|
||||||
|
// Related shifts: upcoming, limit 3
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
const relatedShifts = await database_1.prisma.shift.findMany({
|
||||||
|
where: {
|
||||||
|
date: { gte: today },
|
||||||
|
status: 'OPEN',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
date: true,
|
||||||
|
startTime: true,
|
||||||
|
location: true,
|
||||||
|
maxVolunteers: true,
|
||||||
|
_count: { select: { signups: true } },
|
||||||
|
},
|
||||||
|
orderBy: { date: 'asc' },
|
||||||
|
take: 3,
|
||||||
|
});
|
||||||
|
const result = {
|
||||||
|
campaigns: relatedCampaigns.map(c => ({
|
||||||
|
id: c.id,
|
||||||
|
slug: c.slug,
|
||||||
|
title: c.title,
|
||||||
|
description: c.description?.slice(0, 150) ?? null,
|
||||||
|
emailCount: c._count.emails,
|
||||||
|
})),
|
||||||
|
shifts: relatedShifts.map(s => ({
|
||||||
|
id: s.id,
|
||||||
|
title: s.title,
|
||||||
|
startTime: `${s.date.toISOString().split('T')[0]}T${s.startTime}:00`,
|
||||||
|
location: s.location,
|
||||||
|
currentVolunteers: s._count.signups,
|
||||||
|
maxVolunteers: s.maxVolunteers,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await redis_1.redis.setex(cacheKey, 300, JSON.stringify(result));
|
||||||
|
}
|
||||||
|
catch { /* non-critical */ }
|
||||||
|
res.json(result);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
//# sourceMappingURL=campaigns-public.routes.js.map
|
//# sourceMappingURL=campaigns-public.routes.js.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"campaigns-public.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns-public.routes.ts"],"names":[],"mappings":";;;AAAA,qCAAkE;AAClE,2DAAuD;AAEvD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AA6BL,sCAAoB;AA3BvC,iEAAiE;AACjE,MAAM,CAAC,GAAG,CACR,SAAS,EACT,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,oCAAgB,CAAC,mBAAmB,EAAE,CAAC;QAC/D,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACtB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,wEAAwE;AACxE,MAAM,CAAC,GAAG,CACR,gBAAgB,EAChB,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACxD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAc,CAAC;QACvC,MAAM,QAAQ,GAAG,MAAM,oCAAgB,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC/D,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC"}
|
{"version":3,"file":"campaigns-public.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns-public.routes.ts"],"names":[],"mappings":"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"}
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"campaigns.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.routes.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA6ExB,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,CAAC"}
|
{"version":3,"file":"campaigns.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA6ExB,OAAO,EAAE,MAAM,IAAI,eAAe,EAAE,CAAC"}
|
||||||
@ -2,18 +2,17 @@
|
|||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.campaignsRouter = void 0;
|
exports.campaignsRouter = void 0;
|
||||||
const express_1 = require("express");
|
const express_1 = require("express");
|
||||||
const client_1 = require("@prisma/client");
|
|
||||||
const campaigns_service_1 = require("./campaigns.service");
|
const campaigns_service_1 = require("./campaigns.service");
|
||||||
const campaigns_schemas_1 = require("./campaigns.schemas");
|
const campaigns_schemas_1 = require("./campaigns.schemas");
|
||||||
const validate_1 = require("../../../middleware/validate");
|
const validate_1 = require("../../../middleware/validate");
|
||||||
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
||||||
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
||||||
const ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
|
const roles_1 = require("../../../utils/roles");
|
||||||
const router = (0, express_1.Router)();
|
const router = (0, express_1.Router)();
|
||||||
exports.campaignsRouter = router;
|
exports.campaignsRouter = router;
|
||||||
// All campaign admin routes require authentication + admin role
|
// All campaign admin routes require authentication + admin role
|
||||||
router.use(auth_middleware_1.authenticate);
|
router.use(auth_middleware_1.authenticate);
|
||||||
router.use((0, rbac_middleware_1.requireRole)(...ADMIN_ROLES));
|
router.use((0, rbac_middleware_1.requireRole)(...roles_1.INFLUENCE_ROLES));
|
||||||
// GET /api/campaigns — list campaigns with pagination/filters
|
// GET /api/campaigns — list campaigns with pagination/filters
|
||||||
router.get('/', (0, validate_1.validate)(campaigns_schemas_1.listCampaignsSchema, 'query'), async (req, res, next) => {
|
router.get('/', (0, validate_1.validate)(campaigns_schemas_1.listCampaignsSchema, 'query'), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"campaigns.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.routes.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"campaigns.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.routes.ts"],"names":[],"mappings":"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"}
|
||||||
@ -27,8 +27,9 @@ export declare const createCampaignSchema: z.ZodObject<{
|
|||||||
showResponseWall: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
showResponseWall: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||||
highlightCampaign: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
highlightCampaign: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
||||||
coverPhoto: z.ZodOptional<z.ZodString>;
|
coverPhoto: z.ZodOptional<z.ZodString>;
|
||||||
|
coverVideoId: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
status: "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED";
|
status: "ACTIVE" | "ARCHIVED" | "DRAFT" | "PAUSED";
|
||||||
title: string;
|
title: string;
|
||||||
emailSubject: string;
|
emailSubject: string;
|
||||||
emailBody: string;
|
emailBody: string;
|
||||||
@ -43,16 +44,18 @@ export declare const createCampaignSchema: z.ZodObject<{
|
|||||||
highlightCampaign: boolean;
|
highlightCampaign: boolean;
|
||||||
targetGovernmentLevels: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[];
|
targetGovernmentLevels: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[];
|
||||||
description?: string | undefined;
|
description?: string | undefined;
|
||||||
callToAction?: string | undefined;
|
|
||||||
coverPhoto?: string | undefined;
|
coverPhoto?: string | undefined;
|
||||||
|
coverVideoId?: number | null | undefined;
|
||||||
|
callToAction?: string | undefined;
|
||||||
}, {
|
}, {
|
||||||
title: string;
|
title: string;
|
||||||
emailSubject: string;
|
emailSubject: string;
|
||||||
emailBody: string;
|
emailBody: string;
|
||||||
status?: "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED" | undefined;
|
status?: "ACTIVE" | "ARCHIVED" | "DRAFT" | "PAUSED" | undefined;
|
||||||
description?: string | undefined;
|
description?: string | undefined;
|
||||||
callToAction?: string | undefined;
|
|
||||||
coverPhoto?: string | undefined;
|
coverPhoto?: string | undefined;
|
||||||
|
coverVideoId?: number | null | undefined;
|
||||||
|
callToAction?: string | undefined;
|
||||||
allowSmtpEmail?: boolean | undefined;
|
allowSmtpEmail?: boolean | undefined;
|
||||||
allowMailtoLink?: boolean | undefined;
|
allowMailtoLink?: boolean | undefined;
|
||||||
collectUserInfo?: boolean | undefined;
|
collectUserInfo?: boolean | undefined;
|
||||||
@ -92,14 +95,16 @@ export declare const updateCampaignSchema: z.ZodObject<{
|
|||||||
showResponseWall: z.ZodOptional<z.ZodBoolean>;
|
showResponseWall: z.ZodOptional<z.ZodBoolean>;
|
||||||
highlightCampaign: z.ZodOptional<z.ZodBoolean>;
|
highlightCampaign: z.ZodOptional<z.ZodBoolean>;
|
||||||
coverPhoto: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
coverPhoto: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||||
|
coverVideoId: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
status?: "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED" | undefined;
|
status?: "ACTIVE" | "ARCHIVED" | "DRAFT" | "PAUSED" | undefined;
|
||||||
title?: string | undefined;
|
title?: string | undefined;
|
||||||
description?: string | null | undefined;
|
description?: string | null | undefined;
|
||||||
|
coverPhoto?: string | null | undefined;
|
||||||
|
coverVideoId?: number | null | undefined;
|
||||||
emailSubject?: string | undefined;
|
emailSubject?: string | undefined;
|
||||||
emailBody?: string | undefined;
|
emailBody?: string | undefined;
|
||||||
callToAction?: string | null | undefined;
|
callToAction?: string | null | undefined;
|
||||||
coverPhoto?: string | null | undefined;
|
|
||||||
allowSmtpEmail?: boolean | undefined;
|
allowSmtpEmail?: boolean | undefined;
|
||||||
allowMailtoLink?: boolean | undefined;
|
allowMailtoLink?: boolean | undefined;
|
||||||
collectUserInfo?: boolean | undefined;
|
collectUserInfo?: boolean | undefined;
|
||||||
@ -111,13 +116,14 @@ export declare const updateCampaignSchema: z.ZodObject<{
|
|||||||
highlightCampaign?: boolean | undefined;
|
highlightCampaign?: boolean | undefined;
|
||||||
targetGovernmentLevels?: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[] | undefined;
|
targetGovernmentLevels?: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[] | undefined;
|
||||||
}, {
|
}, {
|
||||||
status?: "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED" | undefined;
|
status?: "ACTIVE" | "ARCHIVED" | "DRAFT" | "PAUSED" | undefined;
|
||||||
title?: string | undefined;
|
title?: string | undefined;
|
||||||
description?: string | null | undefined;
|
description?: string | null | undefined;
|
||||||
|
coverPhoto?: string | null | undefined;
|
||||||
|
coverVideoId?: number | null | undefined;
|
||||||
emailSubject?: string | undefined;
|
emailSubject?: string | undefined;
|
||||||
emailBody?: string | undefined;
|
emailBody?: string | undefined;
|
||||||
callToAction?: string | null | undefined;
|
callToAction?: string | null | undefined;
|
||||||
coverPhoto?: string | null | undefined;
|
|
||||||
allowSmtpEmail?: boolean | undefined;
|
allowSmtpEmail?: boolean | undefined;
|
||||||
allowMailtoLink?: boolean | undefined;
|
allowMailtoLink?: boolean | undefined;
|
||||||
collectUserInfo?: boolean | undefined;
|
collectUserInfo?: boolean | undefined;
|
||||||
@ -142,10 +148,10 @@ export declare const listCampaignsSchema: z.ZodObject<{
|
|||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
limit: number;
|
limit: number;
|
||||||
page: number;
|
page: number;
|
||||||
status?: "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED" | undefined;
|
status?: "ACTIVE" | "ARCHIVED" | "DRAFT" | "PAUSED" | undefined;
|
||||||
search?: string | undefined;
|
search?: string | undefined;
|
||||||
}, {
|
}, {
|
||||||
status?: "ACTIVE" | "DRAFT" | "PAUSED" | "ARCHIVED" | undefined;
|
status?: "ACTIVE" | "ARCHIVED" | "DRAFT" | "PAUSED" | undefined;
|
||||||
search?: string | undefined;
|
search?: string | undefined;
|
||||||
limit?: number | undefined;
|
limit?: number | undefined;
|
||||||
page?: number | undefined;
|
page?: number | undefined;
|
||||||
@ -157,7 +163,99 @@ export declare const campaignIdSchema: z.ZodObject<{
|
|||||||
}, {
|
}, {
|
||||||
id: string;
|
id: string;
|
||||||
}>;
|
}>;
|
||||||
|
export declare const createUserCampaignSchema: z.ZodObject<{
|
||||||
|
title: z.ZodString;
|
||||||
|
description: z.ZodOptional<z.ZodString>;
|
||||||
|
emailSubject: z.ZodString;
|
||||||
|
emailBody: z.ZodString;
|
||||||
|
callToAction: z.ZodOptional<z.ZodString>;
|
||||||
|
targetGovernmentLevels: z.ZodArray<z.ZodNativeEnum<{
|
||||||
|
FEDERAL: "FEDERAL";
|
||||||
|
PROVINCIAL: "PROVINCIAL";
|
||||||
|
MUNICIPAL: "MUNICIPAL";
|
||||||
|
SCHOOL_BOARD: "SCHOOL_BOARD";
|
||||||
|
}>, "many">;
|
||||||
|
}, "strip", z.ZodTypeAny, {
|
||||||
|
title: string;
|
||||||
|
emailSubject: string;
|
||||||
|
emailBody: string;
|
||||||
|
targetGovernmentLevels: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[];
|
||||||
|
description?: string | undefined;
|
||||||
|
callToAction?: string | undefined;
|
||||||
|
}, {
|
||||||
|
title: string;
|
||||||
|
emailSubject: string;
|
||||||
|
emailBody: string;
|
||||||
|
targetGovernmentLevels: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[];
|
||||||
|
description?: string | undefined;
|
||||||
|
callToAction?: string | undefined;
|
||||||
|
}>;
|
||||||
|
export declare const updateUserCampaignSchema: z.ZodObject<{
|
||||||
|
title: z.ZodOptional<z.ZodString>;
|
||||||
|
description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
||||||
|
emailSubject: z.ZodOptional<z.ZodString>;
|
||||||
|
emailBody: z.ZodOptional<z.ZodString>;
|
||||||
|
callToAction: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
||||||
|
targetGovernmentLevels: z.ZodOptional<z.ZodArray<z.ZodNativeEnum<{
|
||||||
|
FEDERAL: "FEDERAL";
|
||||||
|
PROVINCIAL: "PROVINCIAL";
|
||||||
|
MUNICIPAL: "MUNICIPAL";
|
||||||
|
SCHOOL_BOARD: "SCHOOL_BOARD";
|
||||||
|
}>, "many">>;
|
||||||
|
}, "strip", z.ZodTypeAny, {
|
||||||
|
title?: string | undefined;
|
||||||
|
description?: string | undefined;
|
||||||
|
emailSubject?: string | undefined;
|
||||||
|
emailBody?: string | undefined;
|
||||||
|
callToAction?: string | undefined;
|
||||||
|
targetGovernmentLevels?: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[] | undefined;
|
||||||
|
}, {
|
||||||
|
title?: string | undefined;
|
||||||
|
description?: string | undefined;
|
||||||
|
emailSubject?: string | undefined;
|
||||||
|
emailBody?: string | undefined;
|
||||||
|
callToAction?: string | undefined;
|
||||||
|
targetGovernmentLevels?: ("FEDERAL" | "PROVINCIAL" | "MUNICIPAL" | "SCHOOL_BOARD")[] | undefined;
|
||||||
|
}>;
|
||||||
|
export declare const moderateCampaignSchema: z.ZodObject<{
|
||||||
|
action: z.ZodEnum<["approve", "reject", "request_changes"]>;
|
||||||
|
reason: z.ZodOptional<z.ZodString>;
|
||||||
|
notes: z.ZodOptional<z.ZodString>;
|
||||||
|
}, "strip", z.ZodTypeAny, {
|
||||||
|
action: "approve" | "reject" | "request_changes";
|
||||||
|
reason?: string | undefined;
|
||||||
|
notes?: string | undefined;
|
||||||
|
}, {
|
||||||
|
action: "approve" | "reject" | "request_changes";
|
||||||
|
reason?: string | undefined;
|
||||||
|
notes?: string | undefined;
|
||||||
|
}>;
|
||||||
|
export declare const listModerationQueueSchema: z.ZodObject<{
|
||||||
|
page: z.ZodDefault<z.ZodNumber>;
|
||||||
|
limit: z.ZodDefault<z.ZodNumber>;
|
||||||
|
search: z.ZodOptional<z.ZodString>;
|
||||||
|
moderationStatus: z.ZodOptional<z.ZodNativeEnum<{
|
||||||
|
PENDING_REVIEW: "PENDING_REVIEW";
|
||||||
|
APPROVED: "APPROVED";
|
||||||
|
REJECTED: "REJECTED";
|
||||||
|
CHANGES_REQUESTED: "CHANGES_REQUESTED";
|
||||||
|
}>>;
|
||||||
|
}, "strip", z.ZodTypeAny, {
|
||||||
|
limit: number;
|
||||||
|
page: number;
|
||||||
|
search?: string | undefined;
|
||||||
|
moderationStatus?: "APPROVED" | "PENDING_REVIEW" | "REJECTED" | "CHANGES_REQUESTED" | undefined;
|
||||||
|
}, {
|
||||||
|
search?: string | undefined;
|
||||||
|
limit?: number | undefined;
|
||||||
|
page?: number | undefined;
|
||||||
|
moderationStatus?: "APPROVED" | "PENDING_REVIEW" | "REJECTED" | "CHANGES_REQUESTED" | undefined;
|
||||||
|
}>;
|
||||||
export type CreateCampaignInput = z.infer<typeof createCampaignSchema>;
|
export type CreateCampaignInput = z.infer<typeof createCampaignSchema>;
|
||||||
export type UpdateCampaignInput = z.infer<typeof updateCampaignSchema>;
|
export type UpdateCampaignInput = z.infer<typeof updateCampaignSchema>;
|
||||||
export type ListCampaignsInput = z.infer<typeof listCampaignsSchema>;
|
export type ListCampaignsInput = z.infer<typeof listCampaignsSchema>;
|
||||||
|
export type CreateUserCampaignInput = z.infer<typeof createUserCampaignSchema>;
|
||||||
|
export type UpdateUserCampaignInput = z.infer<typeof updateUserCampaignSchema>;
|
||||||
|
export type ModerateCampaignInput = z.infer<typeof moderateCampaignSchema>;
|
||||||
|
export type ListModerationQueueInput = z.infer<typeof listModerationQueueSchema>;
|
||||||
//# sourceMappingURL=campaigns.schemas.d.ts.map
|
//# sourceMappingURL=campaigns.schemas.d.ts.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"campaigns.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkB/B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkB/B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;EAK9B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;EAE3B,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACvE,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACvE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
{"version":3,"file":"campaigns.schemas.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB/B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB/B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;EAK9B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;EAE3B,CAAC;AAGH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;EAOnC,CAAC;AAGH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;EAAqC,CAAC;AAG3E,eAAO,MAAM,sBAAsB;;;;;;;;;;;;EAIjC,CAAC;AAGH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;EAKpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACvE,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACvE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACrE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC/E,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC/E,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC3E,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
|
||||||
@ -1,14 +1,14 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.campaignIdSchema = exports.listCampaignsSchema = exports.updateCampaignSchema = exports.createCampaignSchema = void 0;
|
exports.listModerationQueueSchema = exports.moderateCampaignSchema = exports.updateUserCampaignSchema = exports.createUserCampaignSchema = exports.campaignIdSchema = exports.listCampaignsSchema = exports.updateCampaignSchema = exports.createCampaignSchema = void 0;
|
||||||
const zod_1 = require("zod");
|
const zod_1 = require("zod");
|
||||||
const client_1 = require("@prisma/client");
|
const client_1 = require("@prisma/client");
|
||||||
exports.createCampaignSchema = zod_1.z.object({
|
exports.createCampaignSchema = zod_1.z.object({
|
||||||
title: zod_1.z.string().min(1, 'Title is required'),
|
title: zod_1.z.string().min(1, 'Title is required').max(200),
|
||||||
description: zod_1.z.string().optional(),
|
description: zod_1.z.string().max(2000).optional(),
|
||||||
emailSubject: zod_1.z.string().min(1, 'Email subject is required'),
|
emailSubject: zod_1.z.string().min(1, 'Email subject is required').max(200),
|
||||||
emailBody: zod_1.z.string().min(1, 'Email body is required'),
|
emailBody: zod_1.z.string().min(1, 'Email body is required').max(10000),
|
||||||
callToAction: zod_1.z.string().optional(),
|
callToAction: zod_1.z.string().max(500).optional(),
|
||||||
status: zod_1.z.nativeEnum(client_1.CampaignStatus).optional().default(client_1.CampaignStatus.DRAFT),
|
status: zod_1.z.nativeEnum(client_1.CampaignStatus).optional().default(client_1.CampaignStatus.DRAFT),
|
||||||
targetGovernmentLevels: zod_1.z.array(zod_1.z.nativeEnum(client_1.GovernmentLevel)).optional().default([]),
|
targetGovernmentLevels: zod_1.z.array(zod_1.z.nativeEnum(client_1.GovernmentLevel)).optional().default([]),
|
||||||
allowSmtpEmail: zod_1.z.boolean().optional().default(true),
|
allowSmtpEmail: zod_1.z.boolean().optional().default(true),
|
||||||
@ -20,14 +20,15 @@ exports.createCampaignSchema = zod_1.z.object({
|
|||||||
allowCustomRecipients: zod_1.z.boolean().optional().default(false),
|
allowCustomRecipients: zod_1.z.boolean().optional().default(false),
|
||||||
showResponseWall: zod_1.z.boolean().optional().default(false),
|
showResponseWall: zod_1.z.boolean().optional().default(false),
|
||||||
highlightCampaign: zod_1.z.boolean().optional().default(false),
|
highlightCampaign: zod_1.z.boolean().optional().default(false),
|
||||||
coverPhoto: zod_1.z.string().optional(),
|
coverPhoto: zod_1.z.string().url().max(500).optional(),
|
||||||
|
coverVideoId: zod_1.z.number().int().positive().nullable().optional(),
|
||||||
});
|
});
|
||||||
exports.updateCampaignSchema = zod_1.z.object({
|
exports.updateCampaignSchema = zod_1.z.object({
|
||||||
title: zod_1.z.string().min(1).optional(),
|
title: zod_1.z.string().min(1).max(200).optional(),
|
||||||
description: zod_1.z.string().nullable().optional(),
|
description: zod_1.z.string().max(2000).nullable().optional(),
|
||||||
emailSubject: zod_1.z.string().min(1).optional(),
|
emailSubject: zod_1.z.string().min(1).max(200).optional(),
|
||||||
emailBody: zod_1.z.string().min(1).optional(),
|
emailBody: zod_1.z.string().min(1).max(10000).optional(),
|
||||||
callToAction: zod_1.z.string().nullable().optional(),
|
callToAction: zod_1.z.string().max(500).nullable().optional(),
|
||||||
status: zod_1.z.nativeEnum(client_1.CampaignStatus).optional(),
|
status: zod_1.z.nativeEnum(client_1.CampaignStatus).optional(),
|
||||||
targetGovernmentLevels: zod_1.z.array(zod_1.z.nativeEnum(client_1.GovernmentLevel)).optional(),
|
targetGovernmentLevels: zod_1.z.array(zod_1.z.nativeEnum(client_1.GovernmentLevel)).optional(),
|
||||||
allowSmtpEmail: zod_1.z.boolean().optional(),
|
allowSmtpEmail: zod_1.z.boolean().optional(),
|
||||||
@ -39,7 +40,8 @@ exports.updateCampaignSchema = zod_1.z.object({
|
|||||||
allowCustomRecipients: zod_1.z.boolean().optional(),
|
allowCustomRecipients: zod_1.z.boolean().optional(),
|
||||||
showResponseWall: zod_1.z.boolean().optional(),
|
showResponseWall: zod_1.z.boolean().optional(),
|
||||||
highlightCampaign: zod_1.z.boolean().optional(),
|
highlightCampaign: zod_1.z.boolean().optional(),
|
||||||
coverPhoto: zod_1.z.string().nullable().optional(),
|
coverPhoto: zod_1.z.string().url().max(500).nullable().optional(),
|
||||||
|
coverVideoId: zod_1.z.number().int().positive().nullable().optional(),
|
||||||
});
|
});
|
||||||
exports.listCampaignsSchema = zod_1.z.object({
|
exports.listCampaignsSchema = zod_1.z.object({
|
||||||
page: zod_1.z.coerce.number().int().positive().default(1),
|
page: zod_1.z.coerce.number().int().positive().default(1),
|
||||||
@ -50,4 +52,28 @@ exports.listCampaignsSchema = zod_1.z.object({
|
|||||||
exports.campaignIdSchema = zod_1.z.object({
|
exports.campaignIdSchema = zod_1.z.object({
|
||||||
id: zod_1.z.string().min(1),
|
id: zod_1.z.string().min(1),
|
||||||
});
|
});
|
||||||
|
// User-submitted campaign (restricted fields)
|
||||||
|
exports.createUserCampaignSchema = zod_1.z.object({
|
||||||
|
title: zod_1.z.string().min(3, 'Title must be at least 3 characters').max(200),
|
||||||
|
description: zod_1.z.string().max(2000).optional(),
|
||||||
|
emailSubject: zod_1.z.string().min(3, 'Email subject is required').max(200),
|
||||||
|
emailBody: zod_1.z.string().min(10, 'Email body must be at least 10 characters').max(5000),
|
||||||
|
callToAction: zod_1.z.string().max(500).optional(),
|
||||||
|
targetGovernmentLevels: zod_1.z.array(zod_1.z.nativeEnum(client_1.GovernmentLevel)).min(1, 'Select at least one government level'),
|
||||||
|
});
|
||||||
|
// Update own user campaign (same restricted fields)
|
||||||
|
exports.updateUserCampaignSchema = exports.createUserCampaignSchema.partial();
|
||||||
|
// Admin moderation action
|
||||||
|
exports.moderateCampaignSchema = zod_1.z.object({
|
||||||
|
action: zod_1.z.enum(['approve', 'reject', 'request_changes']),
|
||||||
|
reason: zod_1.z.string().max(2000).optional(),
|
||||||
|
notes: zod_1.z.string().max(2000).optional(),
|
||||||
|
});
|
||||||
|
// Moderation queue filters
|
||||||
|
exports.listModerationQueueSchema = zod_1.z.object({
|
||||||
|
page: zod_1.z.coerce.number().int().positive().default(1),
|
||||||
|
limit: zod_1.z.coerce.number().int().positive().max(100).default(20),
|
||||||
|
search: zod_1.z.string().optional(),
|
||||||
|
moderationStatus: zod_1.z.nativeEnum(client_1.CampaignModerationStatus).optional(),
|
||||||
|
});
|
||||||
//# sourceMappingURL=campaigns.schemas.js.map
|
//# sourceMappingURL=campaigns.schemas.js.map
|
||||||
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
|||||||
import { UserRole } from '@prisma/client';
|
import { UserRole } from '@prisma/client';
|
||||||
import type { CreateCampaignInput, UpdateCampaignInput, ListCampaignsInput } from './campaigns.schemas';
|
import type { CreateCampaignInput, UpdateCampaignInput, ListCampaignsInput, CreateUserCampaignInput, ModerateCampaignInput, ListModerationQueueInput } from './campaigns.schemas';
|
||||||
interface AuthUser {
|
interface AuthUser {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -19,10 +19,13 @@ export declare const campaignsService: {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
coverPhoto: string | null;
|
||||||
|
coverVideoId: number | null;
|
||||||
|
moderationNotes: string | null;
|
||||||
|
createdByUserId: string | null;
|
||||||
emailSubject: string;
|
emailSubject: string;
|
||||||
emailBody: string;
|
emailBody: string;
|
||||||
callToAction: string | null;
|
callToAction: string | null;
|
||||||
coverPhoto: string | null;
|
|
||||||
allowSmtpEmail: boolean;
|
allowSmtpEmail: boolean;
|
||||||
allowMailtoLink: boolean;
|
allowMailtoLink: boolean;
|
||||||
collectUserInfo: boolean;
|
collectUserInfo: boolean;
|
||||||
@ -35,7 +38,11 @@ export declare const campaignsService: {
|
|||||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||||
createdByUserEmail: string | null;
|
createdByUserEmail: string | null;
|
||||||
createdByUserName: string | null;
|
createdByUserName: string | null;
|
||||||
createdByUserId: string | null;
|
isUserGenerated: boolean;
|
||||||
|
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||||
|
reviewedByUserId: string | null;
|
||||||
|
reviewedAt: Date | null;
|
||||||
|
rejectionReason: string | null;
|
||||||
}[];
|
}[];
|
||||||
pagination: {
|
pagination: {
|
||||||
page: number;
|
page: number;
|
||||||
@ -56,10 +63,13 @@ export declare const campaignsService: {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
coverPhoto: string | null;
|
||||||
|
coverVideoId: number | null;
|
||||||
|
moderationNotes: string | null;
|
||||||
|
createdByUserId: string | null;
|
||||||
emailSubject: string;
|
emailSubject: string;
|
||||||
emailBody: string;
|
emailBody: string;
|
||||||
callToAction: string | null;
|
callToAction: string | null;
|
||||||
coverPhoto: string | null;
|
|
||||||
allowSmtpEmail: boolean;
|
allowSmtpEmail: boolean;
|
||||||
allowMailtoLink: boolean;
|
allowMailtoLink: boolean;
|
||||||
collectUserInfo: boolean;
|
collectUserInfo: boolean;
|
||||||
@ -72,7 +82,11 @@ export declare const campaignsService: {
|
|||||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||||
createdByUserEmail: string | null;
|
createdByUserEmail: string | null;
|
||||||
createdByUserName: string | null;
|
createdByUserName: string | null;
|
||||||
createdByUserId: string | null;
|
isUserGenerated: boolean;
|
||||||
|
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||||
|
reviewedByUserId: string | null;
|
||||||
|
reviewedAt: Date | null;
|
||||||
|
rejectionReason: string | null;
|
||||||
}>;
|
}>;
|
||||||
findBySlug(slug: string): Promise<{
|
findBySlug(slug: string): Promise<{
|
||||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||||
@ -86,10 +100,13 @@ export declare const campaignsService: {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
coverPhoto: string | null;
|
||||||
|
coverVideoId: number | null;
|
||||||
|
moderationNotes: string | null;
|
||||||
|
createdByUserId: string | null;
|
||||||
emailSubject: string;
|
emailSubject: string;
|
||||||
emailBody: string;
|
emailBody: string;
|
||||||
callToAction: string | null;
|
callToAction: string | null;
|
||||||
coverPhoto: string | null;
|
|
||||||
allowSmtpEmail: boolean;
|
allowSmtpEmail: boolean;
|
||||||
allowMailtoLink: boolean;
|
allowMailtoLink: boolean;
|
||||||
collectUserInfo: boolean;
|
collectUserInfo: boolean;
|
||||||
@ -102,7 +119,11 @@ export declare const campaignsService: {
|
|||||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||||
createdByUserEmail: string | null;
|
createdByUserEmail: string | null;
|
||||||
createdByUserName: string | null;
|
createdByUserName: string | null;
|
||||||
createdByUserId: string | null;
|
isUserGenerated: boolean;
|
||||||
|
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||||
|
reviewedByUserId: string | null;
|
||||||
|
reviewedAt: Date | null;
|
||||||
|
rejectionReason: string | null;
|
||||||
}>;
|
}>;
|
||||||
create(data: CreateCampaignInput, user: AuthUser): Promise<{
|
create(data: CreateCampaignInput, user: AuthUser): Promise<{
|
||||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||||
@ -116,10 +137,13 @@ export declare const campaignsService: {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
coverPhoto: string | null;
|
||||||
|
coverVideoId: number | null;
|
||||||
|
moderationNotes: string | null;
|
||||||
|
createdByUserId: string | null;
|
||||||
emailSubject: string;
|
emailSubject: string;
|
||||||
emailBody: string;
|
emailBody: string;
|
||||||
callToAction: string | null;
|
callToAction: string | null;
|
||||||
coverPhoto: string | null;
|
|
||||||
allowSmtpEmail: boolean;
|
allowSmtpEmail: boolean;
|
||||||
allowMailtoLink: boolean;
|
allowMailtoLink: boolean;
|
||||||
collectUserInfo: boolean;
|
collectUserInfo: boolean;
|
||||||
@ -132,7 +156,11 @@ export declare const campaignsService: {
|
|||||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||||
createdByUserEmail: string | null;
|
createdByUserEmail: string | null;
|
||||||
createdByUserName: string | null;
|
createdByUserName: string | null;
|
||||||
createdByUserId: string | null;
|
isUserGenerated: boolean;
|
||||||
|
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||||
|
reviewedByUserId: string | null;
|
||||||
|
reviewedAt: Date | null;
|
||||||
|
rejectionReason: string | null;
|
||||||
}>;
|
}>;
|
||||||
update(id: string, data: UpdateCampaignInput): Promise<{
|
update(id: string, data: UpdateCampaignInput): Promise<{
|
||||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||||
@ -146,10 +174,13 @@ export declare const campaignsService: {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
coverPhoto: string | null;
|
||||||
|
coverVideoId: number | null;
|
||||||
|
moderationNotes: string | null;
|
||||||
|
createdByUserId: string | null;
|
||||||
emailSubject: string;
|
emailSubject: string;
|
||||||
emailBody: string;
|
emailBody: string;
|
||||||
callToAction: string | null;
|
callToAction: string | null;
|
||||||
coverPhoto: string | null;
|
|
||||||
allowSmtpEmail: boolean;
|
allowSmtpEmail: boolean;
|
||||||
allowMailtoLink: boolean;
|
allowMailtoLink: boolean;
|
||||||
collectUserInfo: boolean;
|
collectUserInfo: boolean;
|
||||||
@ -162,7 +193,11 @@ export declare const campaignsService: {
|
|||||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||||
createdByUserEmail: string | null;
|
createdByUserEmail: string | null;
|
||||||
createdByUserName: string | null;
|
createdByUserName: string | null;
|
||||||
createdByUserId: string | null;
|
isUserGenerated: boolean;
|
||||||
|
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||||
|
reviewedByUserId: string | null;
|
||||||
|
reviewedAt: Date | null;
|
||||||
|
rejectionReason: string | null;
|
||||||
}>;
|
}>;
|
||||||
findActiveCampaigns(): Promise<{
|
findActiveCampaigns(): Promise<{
|
||||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||||
@ -176,10 +211,11 @@ export declare const campaignsService: {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
coverPhoto: string | null;
|
||||||
|
coverVideoId: number | null;
|
||||||
emailSubject: string;
|
emailSubject: string;
|
||||||
emailBody: string;
|
emailBody: string;
|
||||||
callToAction: string | null;
|
callToAction: string | null;
|
||||||
coverPhoto: string | null;
|
|
||||||
allowSmtpEmail: boolean;
|
allowSmtpEmail: boolean;
|
||||||
allowMailtoLink: boolean;
|
allowMailtoLink: boolean;
|
||||||
collectUserInfo: boolean;
|
collectUserInfo: boolean;
|
||||||
@ -190,9 +226,9 @@ export declare const campaignsService: {
|
|||||||
showResponseWall: boolean;
|
showResponseWall: boolean;
|
||||||
highlightCampaign: boolean;
|
highlightCampaign: boolean;
|
||||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||||
createdByUserEmail: string | null;
|
|
||||||
createdByUserName: string | null;
|
createdByUserName: string | null;
|
||||||
createdByUserId: string | null;
|
isUserGenerated: boolean;
|
||||||
|
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||||
}[]>;
|
}[]>;
|
||||||
findBySlugPublic(slug: string): Promise<{
|
findBySlugPublic(slug: string): Promise<{
|
||||||
status: import(".prisma/client").$Enums.CampaignStatus;
|
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||||
@ -206,10 +242,45 @@ export declare const campaignsService: {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
coverPhoto: string | null;
|
||||||
|
coverVideoId: number | null;
|
||||||
emailSubject: string;
|
emailSubject: string;
|
||||||
emailBody: string;
|
emailBody: string;
|
||||||
callToAction: string | null;
|
callToAction: string | null;
|
||||||
|
allowSmtpEmail: boolean;
|
||||||
|
allowMailtoLink: boolean;
|
||||||
|
collectUserInfo: boolean;
|
||||||
|
showEmailCount: boolean;
|
||||||
|
showCallCount: boolean;
|
||||||
|
allowEmailEditing: boolean;
|
||||||
|
allowCustomRecipients: boolean;
|
||||||
|
showResponseWall: boolean;
|
||||||
|
highlightCampaign: boolean;
|
||||||
|
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||||
|
createdByUserName: string | null;
|
||||||
|
isUserGenerated: boolean;
|
||||||
|
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||||
|
}>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
createUserCampaign(data: CreateUserCampaignInput, user: AuthUser): Promise<{
|
||||||
|
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
_count: {
|
||||||
|
responses: number;
|
||||||
|
emails: number;
|
||||||
|
};
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
slug: string;
|
||||||
coverPhoto: string | null;
|
coverPhoto: string | null;
|
||||||
|
coverVideoId: number | null;
|
||||||
|
moderationNotes: string | null;
|
||||||
|
createdByUserId: string | null;
|
||||||
|
emailSubject: string;
|
||||||
|
emailBody: string;
|
||||||
|
callToAction: string | null;
|
||||||
allowSmtpEmail: boolean;
|
allowSmtpEmail: boolean;
|
||||||
allowMailtoLink: boolean;
|
allowMailtoLink: boolean;
|
||||||
collectUserInfo: boolean;
|
collectUserInfo: boolean;
|
||||||
@ -222,9 +293,175 @@ export declare const campaignsService: {
|
|||||||
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||||
createdByUserEmail: string | null;
|
createdByUserEmail: string | null;
|
||||||
createdByUserName: string | null;
|
createdByUserName: string | null;
|
||||||
createdByUserId: string | null;
|
isUserGenerated: boolean;
|
||||||
|
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||||
|
reviewedByUserId: string | null;
|
||||||
|
reviewedAt: Date | null;
|
||||||
|
rejectionReason: string | null;
|
||||||
|
}>;
|
||||||
|
findUserCampaigns(userId: string): Promise<{
|
||||||
|
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
_count: {
|
||||||
|
responses: number;
|
||||||
|
emails: number;
|
||||||
|
};
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
slug: string;
|
||||||
|
coverPhoto: string | null;
|
||||||
|
coverVideoId: number | null;
|
||||||
|
moderationNotes: string | null;
|
||||||
|
createdByUserId: string | null;
|
||||||
|
emailSubject: string;
|
||||||
|
emailBody: string;
|
||||||
|
callToAction: string | null;
|
||||||
|
allowSmtpEmail: boolean;
|
||||||
|
allowMailtoLink: boolean;
|
||||||
|
collectUserInfo: boolean;
|
||||||
|
showEmailCount: boolean;
|
||||||
|
showCallCount: boolean;
|
||||||
|
allowEmailEditing: boolean;
|
||||||
|
allowCustomRecipients: boolean;
|
||||||
|
showResponseWall: boolean;
|
||||||
|
highlightCampaign: boolean;
|
||||||
|
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||||
|
createdByUserEmail: string | null;
|
||||||
|
createdByUserName: string | null;
|
||||||
|
isUserGenerated: boolean;
|
||||||
|
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||||
|
reviewedByUserId: string | null;
|
||||||
|
reviewedAt: Date | null;
|
||||||
|
rejectionReason: string | null;
|
||||||
|
}[]>;
|
||||||
|
updateUserCampaign(id: string, data: Partial<CreateUserCampaignInput>, user: AuthUser): Promise<{
|
||||||
|
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
_count: {
|
||||||
|
responses: number;
|
||||||
|
emails: number;
|
||||||
|
};
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
slug: string;
|
||||||
|
coverPhoto: string | null;
|
||||||
|
coverVideoId: number | null;
|
||||||
|
moderationNotes: string | null;
|
||||||
|
createdByUserId: string | null;
|
||||||
|
emailSubject: string;
|
||||||
|
emailBody: string;
|
||||||
|
callToAction: string | null;
|
||||||
|
allowSmtpEmail: boolean;
|
||||||
|
allowMailtoLink: boolean;
|
||||||
|
collectUserInfo: boolean;
|
||||||
|
showEmailCount: boolean;
|
||||||
|
showCallCount: boolean;
|
||||||
|
allowEmailEditing: boolean;
|
||||||
|
allowCustomRecipients: boolean;
|
||||||
|
showResponseWall: boolean;
|
||||||
|
highlightCampaign: boolean;
|
||||||
|
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||||
|
createdByUserEmail: string | null;
|
||||||
|
createdByUserName: string | null;
|
||||||
|
isUserGenerated: boolean;
|
||||||
|
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||||
|
reviewedByUserId: string | null;
|
||||||
|
reviewedAt: Date | null;
|
||||||
|
rejectionReason: string | null;
|
||||||
|
}>;
|
||||||
|
findModerationQueue(filters: ListModerationQueueInput): Promise<{
|
||||||
|
campaigns: {
|
||||||
|
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
_count: {
|
||||||
|
responses: number;
|
||||||
|
emails: number;
|
||||||
|
};
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
slug: string;
|
||||||
|
coverPhoto: string | null;
|
||||||
|
coverVideoId: number | null;
|
||||||
|
moderationNotes: string | null;
|
||||||
|
createdByUserId: string | null;
|
||||||
|
emailSubject: string;
|
||||||
|
emailBody: string;
|
||||||
|
callToAction: string | null;
|
||||||
|
allowSmtpEmail: boolean;
|
||||||
|
allowMailtoLink: boolean;
|
||||||
|
collectUserInfo: boolean;
|
||||||
|
showEmailCount: boolean;
|
||||||
|
showCallCount: boolean;
|
||||||
|
allowEmailEditing: boolean;
|
||||||
|
allowCustomRecipients: boolean;
|
||||||
|
showResponseWall: boolean;
|
||||||
|
highlightCampaign: boolean;
|
||||||
|
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||||
|
createdByUserEmail: string | null;
|
||||||
|
createdByUserName: string | null;
|
||||||
|
isUserGenerated: boolean;
|
||||||
|
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||||
|
reviewedByUserId: string | null;
|
||||||
|
reviewedAt: Date | null;
|
||||||
|
rejectionReason: string | null;
|
||||||
|
}[];
|
||||||
|
pagination: {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
getModerationStats(): Promise<{
|
||||||
|
total: number;
|
||||||
|
pending: number;
|
||||||
|
approved: number;
|
||||||
|
rejected: number;
|
||||||
|
changesRequested: number;
|
||||||
|
}>;
|
||||||
|
moderateCampaign(id: string, input: ModerateCampaignInput, reviewer: AuthUser): Promise<{
|
||||||
|
status: import(".prisma/client").$Enums.CampaignStatus;
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
_count: {
|
||||||
|
responses: number;
|
||||||
|
emails: number;
|
||||||
|
};
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
slug: string;
|
||||||
|
coverPhoto: string | null;
|
||||||
|
coverVideoId: number | null;
|
||||||
|
moderationNotes: string | null;
|
||||||
|
createdByUserId: string | null;
|
||||||
|
emailSubject: string;
|
||||||
|
emailBody: string;
|
||||||
|
callToAction: string | null;
|
||||||
|
allowSmtpEmail: boolean;
|
||||||
|
allowMailtoLink: boolean;
|
||||||
|
collectUserInfo: boolean;
|
||||||
|
showEmailCount: boolean;
|
||||||
|
showCallCount: boolean;
|
||||||
|
allowEmailEditing: boolean;
|
||||||
|
allowCustomRecipients: boolean;
|
||||||
|
showResponseWall: boolean;
|
||||||
|
highlightCampaign: boolean;
|
||||||
|
targetGovernmentLevels: import(".prisma/client").$Enums.GovernmentLevel[];
|
||||||
|
createdByUserEmail: string | null;
|
||||||
|
createdByUserName: string | null;
|
||||||
|
isUserGenerated: boolean;
|
||||||
|
moderationStatus: import(".prisma/client").$Enums.CampaignModerationStatus | null;
|
||||||
|
reviewedByUserId: string | null;
|
||||||
|
reviewedAt: Date | null;
|
||||||
|
rejectionReason: string | null;
|
||||||
}>;
|
}>;
|
||||||
delete(id: string): Promise<void>;
|
|
||||||
};
|
};
|
||||||
export {};
|
export {};
|
||||||
//# sourceMappingURL=campaigns.service.d.ts.map
|
//# sourceMappingURL=campaigns.service.d.ts.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"campaigns.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AA8DxG,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,eAAO,MAAM,gBAAgB;qBACJ,kBAAkB,SAAS,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2CvC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAaF,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAaV,mBAAmB,QAAQ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAgCrC,MAAM,QAAQ,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BA0CrB,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAiBlB,MAAM;CAQxB,CAAC"}
|
{"version":3,"file":"campaigns.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/campaigns/campaigns.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,QAAQ,EAA4B,MAAM,gBAAgB,CAAC;AAI5E,OAAO,KAAK,EACV,mBAAmB,EAAE,mBAAmB,EAAE,kBAAkB,EAC5D,uBAAuB,EAA2B,qBAAqB,EAAE,wBAAwB,EAClG,MAAM,qBAAqB,CAAC;AAiH7B,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,eAAO,MAAM,gBAAgB;qBACJ,kBAAkB,SAAS,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0CvC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAaF,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAaV,mBAAmB,QAAQ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAgCrC,MAAM,QAAQ,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BA0CrB,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAiBlB,MAAM;6BAWQ,uBAAuB,QAAQ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BAwCtC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAQT,MAAM,QAAQ,OAAO,CAAC,uBAAuB,CAAC,QAAQ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iCA2CxD,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBA0ChC,MAAM,SAAS,qBAAqB,YAAY,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCpF,CAAC"}
|
||||||
@ -4,6 +4,15 @@ exports.campaignsService = void 0;
|
|||||||
const client_1 = require("@prisma/client");
|
const client_1 = require("@prisma/client");
|
||||||
const database_1 = require("../../../config/database");
|
const database_1 = require("../../../config/database");
|
||||||
const error_handler_1 = require("../../../middleware/error-handler");
|
const error_handler_1 = require("../../../middleware/error-handler");
|
||||||
|
const roles_1 = require("../../../utils/roles");
|
||||||
|
function escapeHtml(unsafe) {
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
const campaignSelect = {
|
const campaignSelect = {
|
||||||
id: true,
|
id: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
@ -13,6 +22,7 @@ const campaignSelect = {
|
|||||||
emailBody: true,
|
emailBody: true,
|
||||||
callToAction: true,
|
callToAction: true,
|
||||||
coverPhoto: true,
|
coverPhoto: true,
|
||||||
|
coverVideoId: true,
|
||||||
status: true,
|
status: true,
|
||||||
allowSmtpEmail: true,
|
allowSmtpEmail: true,
|
||||||
allowMailtoLink: true,
|
allowMailtoLink: true,
|
||||||
@ -27,6 +37,46 @@ const campaignSelect = {
|
|||||||
createdByUserId: true,
|
createdByUserId: true,
|
||||||
createdByUserEmail: true,
|
createdByUserEmail: true,
|
||||||
createdByUserName: true,
|
createdByUserName: true,
|
||||||
|
isUserGenerated: true,
|
||||||
|
moderationStatus: true,
|
||||||
|
reviewedByUserId: true,
|
||||||
|
reviewedAt: true,
|
||||||
|
rejectionReason: true,
|
||||||
|
moderationNotes: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
emails: true,
|
||||||
|
responses: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
/** Public-facing select — strips admin-only fields (emails, internal IDs, moderation notes) */
|
||||||
|
const publicCampaignSelect = {
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
emailSubject: true,
|
||||||
|
emailBody: true,
|
||||||
|
callToAction: true,
|
||||||
|
coverPhoto: true,
|
||||||
|
coverVideoId: true,
|
||||||
|
status: true,
|
||||||
|
allowSmtpEmail: true,
|
||||||
|
allowMailtoLink: true,
|
||||||
|
collectUserInfo: true,
|
||||||
|
showEmailCount: true,
|
||||||
|
showCallCount: true,
|
||||||
|
allowEmailEditing: true,
|
||||||
|
allowCustomRecipients: true,
|
||||||
|
showResponseWall: true,
|
||||||
|
highlightCampaign: true,
|
||||||
|
targetGovernmentLevels: true,
|
||||||
|
createdByUserName: true,
|
||||||
|
isUserGenerated: true,
|
||||||
|
moderationStatus: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
_count: {
|
_count: {
|
||||||
@ -72,8 +122,7 @@ exports.campaignsService = {
|
|||||||
if (status)
|
if (status)
|
||||||
where.status = status;
|
where.status = status;
|
||||||
// Non-admin users only see their own campaigns
|
// Non-admin users only see their own campaigns
|
||||||
const adminRoles = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
|
if (user && !(0, roles_1.hasAnyRole)(user, roles_1.ADMIN_ROLES)) {
|
||||||
if (user && !adminRoles.includes(user.role)) {
|
|
||||||
where.createdByUserId = user.id;
|
where.createdByUserId = user.id;
|
||||||
}
|
}
|
||||||
const [campaigns, total] = await Promise.all([
|
const [campaigns, total] = await Promise.all([
|
||||||
@ -171,7 +220,7 @@ exports.campaignsService = {
|
|||||||
async findActiveCampaigns() {
|
async findActiveCampaigns() {
|
||||||
return database_1.prisma.campaign.findMany({
|
return database_1.prisma.campaign.findMany({
|
||||||
where: { status: 'ACTIVE' },
|
where: { status: 'ACTIVE' },
|
||||||
select: campaignSelect,
|
select: publicCampaignSelect,
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{ highlightCampaign: 'desc' },
|
{ highlightCampaign: 'desc' },
|
||||||
{ createdAt: 'desc' },
|
{ createdAt: 'desc' },
|
||||||
@ -181,7 +230,7 @@ exports.campaignsService = {
|
|||||||
async findBySlugPublic(slug) {
|
async findBySlugPublic(slug) {
|
||||||
const campaign = await database_1.prisma.campaign.findUnique({
|
const campaign = await database_1.prisma.campaign.findUnique({
|
||||||
where: { slug },
|
where: { slug },
|
||||||
select: campaignSelect,
|
select: publicCampaignSelect,
|
||||||
});
|
});
|
||||||
if (!campaign) {
|
if (!campaign) {
|
||||||
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||||
@ -198,5 +247,162 @@ exports.campaignsService = {
|
|||||||
}
|
}
|
||||||
await database_1.prisma.campaign.delete({ where: { id } });
|
await database_1.prisma.campaign.delete({ where: { id } });
|
||||||
},
|
},
|
||||||
|
// --- User-Generated Campaign Methods ---
|
||||||
|
async createUserCampaign(data, user) {
|
||||||
|
const baseSlug = generateSlug(data.title);
|
||||||
|
const slug = await resolveSlugCollision(baseSlug);
|
||||||
|
const dbUser = await database_1.prisma.user.findUnique({
|
||||||
|
where: { id: user.id },
|
||||||
|
select: { name: true },
|
||||||
|
});
|
||||||
|
const campaign = await database_1.prisma.campaign.create({
|
||||||
|
data: {
|
||||||
|
slug,
|
||||||
|
title: escapeHtml(data.title),
|
||||||
|
description: data.description ? escapeHtml(data.description) : null,
|
||||||
|
emailSubject: escapeHtml(data.emailSubject),
|
||||||
|
emailBody: escapeHtml(data.emailBody),
|
||||||
|
callToAction: data.callToAction ? escapeHtml(data.callToAction) : null,
|
||||||
|
targetGovernmentLevels: data.targetGovernmentLevels,
|
||||||
|
status: 'DRAFT',
|
||||||
|
isUserGenerated: true,
|
||||||
|
moderationStatus: client_1.CampaignModerationStatus.PENDING_REVIEW,
|
||||||
|
allowSmtpEmail: false,
|
||||||
|
allowMailtoLink: true,
|
||||||
|
collectUserInfo: true,
|
||||||
|
showEmailCount: true,
|
||||||
|
showCallCount: false,
|
||||||
|
allowEmailEditing: false,
|
||||||
|
allowCustomRecipients: false,
|
||||||
|
showResponseWall: false,
|
||||||
|
highlightCampaign: false,
|
||||||
|
createdByUserId: user.id,
|
||||||
|
createdByUserEmail: user.email,
|
||||||
|
createdByUserName: dbUser?.name ?? null,
|
||||||
|
},
|
||||||
|
select: campaignSelect,
|
||||||
|
});
|
||||||
|
return campaign;
|
||||||
|
},
|
||||||
|
async findUserCampaigns(userId) {
|
||||||
|
return database_1.prisma.campaign.findMany({
|
||||||
|
where: { createdByUserId: userId, isUserGenerated: true },
|
||||||
|
select: campaignSelect,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async updateUserCampaign(id, data, user) {
|
||||||
|
const existing = await database_1.prisma.campaign.findUnique({ where: { id } });
|
||||||
|
if (!existing) {
|
||||||
|
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||||
|
}
|
||||||
|
if (existing.createdByUserId !== user.id) {
|
||||||
|
throw new error_handler_1.AppError(403, 'You can only edit your own campaigns', 'FORBIDDEN');
|
||||||
|
}
|
||||||
|
if (!existing.isUserGenerated) {
|
||||||
|
throw new error_handler_1.AppError(403, 'Cannot edit admin-created campaigns', 'FORBIDDEN');
|
||||||
|
}
|
||||||
|
if (existing.moderationStatus !== client_1.CampaignModerationStatus.CHANGES_REQUESTED &&
|
||||||
|
existing.moderationStatus !== client_1.CampaignModerationStatus.PENDING_REVIEW) {
|
||||||
|
throw new error_handler_1.AppError(400, 'Campaign cannot be edited in its current state', 'INVALID_STATE');
|
||||||
|
}
|
||||||
|
const updateData = {};
|
||||||
|
if (data.title) {
|
||||||
|
updateData.title = escapeHtml(data.title);
|
||||||
|
const baseSlug = generateSlug(data.title);
|
||||||
|
updateData.slug = await resolveSlugCollision(baseSlug, id);
|
||||||
|
}
|
||||||
|
if (data.description !== undefined)
|
||||||
|
updateData.description = data.description ? escapeHtml(data.description) : null;
|
||||||
|
if (data.emailSubject)
|
||||||
|
updateData.emailSubject = escapeHtml(data.emailSubject);
|
||||||
|
if (data.emailBody)
|
||||||
|
updateData.emailBody = escapeHtml(data.emailBody);
|
||||||
|
if (data.callToAction !== undefined)
|
||||||
|
updateData.callToAction = data.callToAction ? escapeHtml(data.callToAction) : null;
|
||||||
|
if (data.targetGovernmentLevels)
|
||||||
|
updateData.targetGovernmentLevels = data.targetGovernmentLevels;
|
||||||
|
// Reset to pending review on edit
|
||||||
|
updateData.moderationStatus = client_1.CampaignModerationStatus.PENDING_REVIEW;
|
||||||
|
updateData.rejectionReason = null;
|
||||||
|
return database_1.prisma.campaign.update({
|
||||||
|
where: { id },
|
||||||
|
data: updateData,
|
||||||
|
select: campaignSelect,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// --- Moderation Methods ---
|
||||||
|
async findModerationQueue(filters) {
|
||||||
|
const { page, limit, search, moderationStatus } = filters;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
const where = { isUserGenerated: true };
|
||||||
|
if (moderationStatus)
|
||||||
|
where.moderationStatus = moderationStatus;
|
||||||
|
if (search) {
|
||||||
|
where.OR = [
|
||||||
|
{ title: { contains: search, mode: 'insensitive' } },
|
||||||
|
{ createdByUserName: { contains: search, mode: 'insensitive' } },
|
||||||
|
{ createdByUserEmail: { contains: search, mode: 'insensitive' } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const [campaigns, total] = await Promise.all([
|
||||||
|
database_1.prisma.campaign.findMany({
|
||||||
|
where,
|
||||||
|
select: campaignSelect,
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
}),
|
||||||
|
database_1.prisma.campaign.count({ where }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
campaigns,
|
||||||
|
pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async getModerationStats() {
|
||||||
|
const [total, pending, approved, rejected, changesRequested] = await Promise.all([
|
||||||
|
database_1.prisma.campaign.count({ where: { isUserGenerated: true } }),
|
||||||
|
database_1.prisma.campaign.count({ where: { moderationStatus: client_1.CampaignModerationStatus.PENDING_REVIEW } }),
|
||||||
|
database_1.prisma.campaign.count({ where: { moderationStatus: client_1.CampaignModerationStatus.APPROVED } }),
|
||||||
|
database_1.prisma.campaign.count({ where: { moderationStatus: client_1.CampaignModerationStatus.REJECTED } }),
|
||||||
|
database_1.prisma.campaign.count({ where: { moderationStatus: client_1.CampaignModerationStatus.CHANGES_REQUESTED } }),
|
||||||
|
]);
|
||||||
|
return { total, pending, approved, rejected, changesRequested };
|
||||||
|
},
|
||||||
|
async moderateCampaign(id, input, reviewer) {
|
||||||
|
const existing = await database_1.prisma.campaign.findUnique({ where: { id } });
|
||||||
|
if (!existing) {
|
||||||
|
throw new error_handler_1.AppError(404, 'Campaign not found', 'CAMPAIGN_NOT_FOUND');
|
||||||
|
}
|
||||||
|
if (!existing.isUserGenerated) {
|
||||||
|
throw new error_handler_1.AppError(400, 'Only user-generated campaigns can be moderated', 'INVALID_STATE');
|
||||||
|
}
|
||||||
|
const updateData = {
|
||||||
|
reviewedByUserId: reviewer.id,
|
||||||
|
reviewedAt: new Date(),
|
||||||
|
moderationNotes: input.notes ?? null,
|
||||||
|
};
|
||||||
|
switch (input.action) {
|
||||||
|
case 'approve':
|
||||||
|
updateData.moderationStatus = client_1.CampaignModerationStatus.APPROVED;
|
||||||
|
updateData.status = 'ACTIVE';
|
||||||
|
updateData.rejectionReason = null;
|
||||||
|
break;
|
||||||
|
case 'reject':
|
||||||
|
updateData.moderationStatus = client_1.CampaignModerationStatus.REJECTED;
|
||||||
|
updateData.rejectionReason = input.reason ?? null;
|
||||||
|
break;
|
||||||
|
case 'request_changes':
|
||||||
|
updateData.moderationStatus = client_1.CampaignModerationStatus.CHANGES_REQUESTED;
|
||||||
|
updateData.rejectionReason = input.reason ?? null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return database_1.prisma.campaign.update({
|
||||||
|
where: { id },
|
||||||
|
data: updateData,
|
||||||
|
select: campaignSelect,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
//# sourceMappingURL=campaigns.service.js.map
|
//# sourceMappingURL=campaigns.service.js.map
|
||||||
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
{"version":3,"file":"email-queue.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/email-queue/email-queue.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAwDxB,OAAO,EAAE,MAAM,IAAI,gBAAgB,EAAE,CAAC"}
|
{"version":3,"file":"email-queue.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/email-queue/email-queue.routes.ts"],"names":[],"mappings":"AAMA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAwDxB,OAAO,EAAE,MAAM,IAAI,gBAAgB,EAAE,CAAC"}
|
||||||
@ -2,15 +2,14 @@
|
|||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.emailQueueRouter = void 0;
|
exports.emailQueueRouter = void 0;
|
||||||
const express_1 = require("express");
|
const express_1 = require("express");
|
||||||
const client_1 = require("@prisma/client");
|
|
||||||
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
||||||
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
||||||
const email_queue_service_1 = require("../../../services/email-queue.service");
|
const email_queue_service_1 = require("../../../services/email-queue.service");
|
||||||
const ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
|
const roles_1 = require("../../../utils/roles");
|
||||||
const router = (0, express_1.Router)();
|
const router = (0, express_1.Router)();
|
||||||
exports.emailQueueRouter = router;
|
exports.emailQueueRouter = router;
|
||||||
router.use(auth_middleware_1.authenticate);
|
router.use(auth_middleware_1.authenticate);
|
||||||
router.use((0, rbac_middleware_1.requireRole)(...ADMIN_ROLES));
|
router.use((0, rbac_middleware_1.requireRole)(...roles_1.INFLUENCE_ROLES));
|
||||||
// GET /api/email-queue/stats
|
// GET /api/email-queue/stats
|
||||||
router.get('/stats', async (_req, res, next) => {
|
router.get('/stats', async (_req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"email-queue.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/email-queue/email-queue.routes.ts"],"names":[],"mappings":";;;AAAA,qCAAkE;AAClE,2CAA0C;AAC1C,yEAAmE;AACnE,yEAAkE;AAClE,+EAA0E;AAE1E,MAAM,WAAW,GAAe,CAAC,iBAAQ,CAAC,WAAW,EAAE,iBAAQ,CAAC,eAAe,EAAE,iBAAQ,CAAC,SAAS,CAAC,CAAC;AAErG,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAwDL,kCAAgB;AAvDnC,MAAM,CAAC,GAAG,CAAC,8BAAY,CAAC,CAAC;AACzB,MAAM,CAAC,GAAG,CAAC,IAAA,6BAAW,EAAC,GAAG,WAAW,CAAC,CAAC,CAAC;AAExC,6BAA6B;AAC7B,MAAM,CAAC,GAAG,CACR,QAAQ,EACR,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,uCAAiB,CAAC,QAAQ,EAAE,CAAC;QACjD,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,8BAA8B;AAC9B,MAAM,CAAC,IAAI,CACT,QAAQ,EACR,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,uCAAiB,CAAC,KAAK,EAAE,CAAC;QAChC,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,+BAA+B;AAC/B,MAAM,CAAC,IAAI,CACT,SAAS,EACT,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,uCAAiB,CAAC,MAAM,EAAE,CAAC;QACjC,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IACzC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,8BAA8B;AAC9B,MAAM,CAAC,IAAI,CACT,QAAQ,EACR,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,uCAAiB,CAAC,KAAK,EAAE,CAAC;QAChD,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,WAAW,OAAO,iBAAiB,EAAE,OAAO,EAAE,CAAC,CAAC;IACtE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC"}
|
{"version":3,"file":"email-queue.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/email-queue/email-queue.routes.ts"],"names":[],"mappings":";;;AAAA,qCAAkE;AAClE,yEAAmE;AACnE,yEAAkE;AAClE,+EAA0E;AAC1E,gDAAuD;AAEvD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAwDL,kCAAgB;AAvDnC,MAAM,CAAC,GAAG,CAAC,8BAAY,CAAC,CAAC;AACzB,MAAM,CAAC,GAAG,CAAC,IAAA,6BAAW,EAAC,GAAG,uBAAe,CAAC,CAAC,CAAC;AAE5C,6BAA6B;AAC7B,MAAM,CAAC,GAAG,CACR,QAAQ,EACR,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,uCAAiB,CAAC,QAAQ,EAAE,CAAC;QACjD,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,8BAA8B;AAC9B,MAAM,CAAC,IAAI,CACT,QAAQ,EACR,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,uCAAiB,CAAC,KAAK,EAAE,CAAC;QAChC,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,+BAA+B;AAC/B,MAAM,CAAC,IAAI,CACT,SAAS,EACT,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,uCAAiB,CAAC,MAAM,EAAE,CAAC;QACjC,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IACzC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,8BAA8B;AAC9B,MAAM,CAAC,IAAI,CACT,QAAQ,EACR,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,uCAAiB,CAAC,KAAK,EAAE,CAAC;QAChD,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,WAAW,OAAO,iBAAiB,EAAE,OAAO,EAAE,CAAC,CAAC;IACtE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC"}
|
||||||
@ -13,9 +13,9 @@ export declare const postalCodeParamSchema: z.ZodObject<{
|
|||||||
export declare const postalCodeQuerySchema: z.ZodObject<{
|
export declare const postalCodeQuerySchema: z.ZodObject<{
|
||||||
refresh: z.ZodDefault<z.ZodOptional<z.ZodEnum<["true", "false"]>>>;
|
refresh: z.ZodDefault<z.ZodOptional<z.ZodEnum<["true", "false"]>>>;
|
||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
refresh: "true" | "false";
|
refresh: "false" | "true";
|
||||||
}, {
|
}, {
|
||||||
refresh?: "true" | "false" | undefined;
|
refresh?: "false" | "true" | undefined;
|
||||||
}>;
|
}>;
|
||||||
export type PostalCodeParam = z.infer<typeof postalCodeParamSchema>;
|
export type PostalCodeParam = z.infer<typeof postalCodeParamSchema>;
|
||||||
export type PostalCodeQuery = z.infer<typeof postalCodeQuerySchema>;
|
export type PostalCodeQuery = z.infer<typeof postalCodeQuerySchema>;
|
||||||
|
|||||||
@ -12,18 +12,18 @@ export declare const postalCodesService: {
|
|||||||
city: string | null;
|
city: string | null;
|
||||||
postalCode: string;
|
postalCode: string;
|
||||||
province: string | null;
|
province: string | null;
|
||||||
|
lastUpdated: Date;
|
||||||
centroidLat: Prisma.Decimal | null;
|
centroidLat: Prisma.Decimal | null;
|
||||||
centroidLng: Prisma.Decimal | null;
|
centroidLng: Prisma.Decimal | null;
|
||||||
lastUpdated: Date;
|
|
||||||
}>;
|
}>;
|
||||||
findByPostalCode(code: string): Promise<{
|
findByPostalCode(code: string): Promise<{
|
||||||
id: string;
|
id: string;
|
||||||
city: string | null;
|
city: string | null;
|
||||||
postalCode: string;
|
postalCode: string;
|
||||||
province: string | null;
|
province: string | null;
|
||||||
|
lastUpdated: Date;
|
||||||
centroidLat: Prisma.Decimal | null;
|
centroidLat: Prisma.Decimal | null;
|
||||||
centroidLng: Prisma.Decimal | null;
|
centroidLng: Prisma.Decimal | null;
|
||||||
lastUpdated: Date;
|
|
||||||
} | null>;
|
} | null>;
|
||||||
findAll(filters: {
|
findAll(filters: {
|
||||||
page: number;
|
page: number;
|
||||||
@ -35,9 +35,9 @@ export declare const postalCodesService: {
|
|||||||
city: string | null;
|
city: string | null;
|
||||||
postalCode: string;
|
postalCode: string;
|
||||||
province: string | null;
|
province: string | null;
|
||||||
|
lastUpdated: Date;
|
||||||
centroidLat: Prisma.Decimal | null;
|
centroidLat: Prisma.Decimal | null;
|
||||||
centroidLng: Prisma.Decimal | null;
|
centroidLng: Prisma.Decimal | null;
|
||||||
lastUpdated: Date;
|
|
||||||
}[];
|
}[];
|
||||||
pagination: {
|
pagination: {
|
||||||
page: number;
|
page: number;
|
||||||
@ -51,9 +51,9 @@ export declare const postalCodesService: {
|
|||||||
city: string | null;
|
city: string | null;
|
||||||
postalCode: string;
|
postalCode: string;
|
||||||
province: string | null;
|
province: string | null;
|
||||||
|
lastUpdated: Date;
|
||||||
centroidLat: Prisma.Decimal | null;
|
centroidLat: Prisma.Decimal | null;
|
||||||
centroidLng: Prisma.Decimal | null;
|
centroidLng: Prisma.Decimal | null;
|
||||||
lastUpdated: Date;
|
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"representatives.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.routes.ts"],"names":[],"mappings":"AAWA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAiHxB,OAAO,EAAE,MAAM,IAAI,qBAAqB,EAAE,CAAC"}
|
{"version":3,"file":"representatives.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.routes.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAiHxB,OAAO,EAAE,MAAM,IAAI,qBAAqB,EAAE,CAAC"}
|
||||||
@ -2,14 +2,13 @@
|
|||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.representativesRouter = void 0;
|
exports.representativesRouter = void 0;
|
||||||
const express_1 = require("express");
|
const express_1 = require("express");
|
||||||
const client_1 = require("@prisma/client");
|
|
||||||
const representatives_service_1 = require("./representatives.service");
|
const representatives_service_1 = require("./representatives.service");
|
||||||
const representatives_schemas_1 = require("./representatives.schemas");
|
const representatives_schemas_1 = require("./representatives.schemas");
|
||||||
const postal_codes_schemas_1 = require("../postal-codes/postal-codes.schemas");
|
const postal_codes_schemas_1 = require("../postal-codes/postal-codes.schemas");
|
||||||
const validate_1 = require("../../../middleware/validate");
|
const validate_1 = require("../../../middleware/validate");
|
||||||
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
||||||
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
||||||
const ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
|
const roles_1 = require("../../../utils/roles");
|
||||||
const router = (0, express_1.Router)();
|
const router = (0, express_1.Router)();
|
||||||
exports.representativesRouter = router;
|
exports.representativesRouter = router;
|
||||||
// =============================================
|
// =============================================
|
||||||
@ -41,7 +40,7 @@ router.get('/test-connection', async (_req, res, next) => {
|
|||||||
// ADMIN ROUTES (auth + role required)
|
// ADMIN ROUTES (auth + role required)
|
||||||
// =============================================
|
// =============================================
|
||||||
router.use(auth_middleware_1.authenticate);
|
router.use(auth_middleware_1.authenticate);
|
||||||
router.use((0, rbac_middleware_1.requireRole)(...ADMIN_ROLES));
|
router.use((0, rbac_middleware_1.requireRole)(...roles_1.INFLUENCE_ROLES));
|
||||||
// GET /api/representatives/cache-stats — cache statistics
|
// GET /api/representatives/cache-stats — cache statistics
|
||||||
router.get('/cache-stats', async (_req, res, next) => {
|
router.get('/cache-stats', async (_req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"representatives.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.routes.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"representatives.routes.js","sourceRoot":"","sources":["../../../../src/modules/influence/representatives/representatives.routes.ts"],"names":[],"mappings":"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"}
|
||||||
@ -14,13 +14,13 @@ export declare const representativesService: {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
url: string | null;
|
url: string | null;
|
||||||
postalCode: string;
|
postalCode: string;
|
||||||
|
cachedAt: Date;
|
||||||
districtName: string | null;
|
districtName: string | null;
|
||||||
electedOffice: string | null;
|
electedOffice: string | null;
|
||||||
partyName: string | null;
|
partyName: string | null;
|
||||||
representativeSetName: string | null;
|
representativeSetName: string | null;
|
||||||
photoUrl: string | null;
|
photoUrl: string | null;
|
||||||
offices: Prisma.JsonValue | null;
|
offices: Prisma.JsonValue | null;
|
||||||
cachedAt: Date;
|
|
||||||
}[];
|
}[];
|
||||||
} | {
|
} | {
|
||||||
source: "api";
|
source: "api";
|
||||||
@ -51,13 +51,13 @@ export declare const representativesService: {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
url: string | null;
|
url: string | null;
|
||||||
postalCode: string;
|
postalCode: string;
|
||||||
|
cachedAt: Date;
|
||||||
districtName: string | null;
|
districtName: string | null;
|
||||||
electedOffice: string | null;
|
electedOffice: string | null;
|
||||||
partyName: string | null;
|
partyName: string | null;
|
||||||
representativeSetName: string | null;
|
representativeSetName: string | null;
|
||||||
photoUrl: string | null;
|
photoUrl: string | null;
|
||||||
offices: Prisma.JsonValue | null;
|
offices: Prisma.JsonValue | null;
|
||||||
cachedAt: Date;
|
|
||||||
}[];
|
}[];
|
||||||
pagination: {
|
pagination: {
|
||||||
page: number;
|
page: number;
|
||||||
@ -72,13 +72,13 @@ export declare const representativesService: {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
url: string | null;
|
url: string | null;
|
||||||
postalCode: string;
|
postalCode: string;
|
||||||
|
cachedAt: Date;
|
||||||
districtName: string | null;
|
districtName: string | null;
|
||||||
electedOffice: string | null;
|
electedOffice: string | null;
|
||||||
partyName: string | null;
|
partyName: string | null;
|
||||||
representativeSetName: string | null;
|
representativeSetName: string | null;
|
||||||
photoUrl: string | null;
|
photoUrl: string | null;
|
||||||
offices: Prisma.JsonValue | null;
|
offices: Prisma.JsonValue | null;
|
||||||
cachedAt: Date;
|
|
||||||
}>;
|
}>;
|
||||||
clearByPostalCode(code: string): Promise<{
|
clearByPostalCode(code: string): Promise<{
|
||||||
deleted: number;
|
deleted: number;
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"responses.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/responses/responses.routes.ts"],"names":[],"mappings":"AAkBA,QAAA,MAAM,oBAAoB,4CAAW,CAAC;AAiDtC,QAAA,MAAM,qBAAqB,4CAAW,CAAC;AA6EvC,QAAA,MAAM,oBAAoB,4CAAW,CAAC;AA6DtC,OAAO,EAAE,oBAAoB,IAAI,4BAA4B,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC"}
|
{"version":3,"file":"responses.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/responses/responses.routes.ts"],"names":[],"mappings":"AAgBA,QAAA,MAAM,oBAAoB,4CAAW,CAAC;AAiDtC,QAAA,MAAM,qBAAqB,4CAAW,CAAC;AA6EvC,QAAA,MAAM,oBAAoB,4CAAW,CAAC;AA6DtC,OAAO,EAAE,oBAAoB,IAAI,4BAA4B,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,CAAC"}
|
||||||
@ -2,7 +2,6 @@
|
|||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.responsesAdminRouter = exports.responsesPublicRouter = exports.responseCampaignPublicRouter = void 0;
|
exports.responsesAdminRouter = exports.responsesPublicRouter = exports.responseCampaignPublicRouter = void 0;
|
||||||
const express_1 = require("express");
|
const express_1 = require("express");
|
||||||
const client_1 = require("@prisma/client");
|
|
||||||
const responses_service_1 = require("./responses.service");
|
const responses_service_1 = require("./responses.service");
|
||||||
const responses_schemas_1 = require("./responses.schemas");
|
const responses_schemas_1 = require("./responses.schemas");
|
||||||
const validate_1 = require("../../../middleware/validate");
|
const validate_1 = require("../../../middleware/validate");
|
||||||
@ -10,7 +9,7 @@ const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
|||||||
const auth_middleware_2 = require("../../../middleware/auth.middleware");
|
const auth_middleware_2 = require("../../../middleware/auth.middleware");
|
||||||
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
||||||
const rate_limit_1 = require("../../../middleware/rate-limit");
|
const rate_limit_1 = require("../../../middleware/rate-limit");
|
||||||
const ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN, client_1.UserRole.MAP_ADMIN];
|
const roles_1 = require("../../../utils/roles");
|
||||||
// --- Campaign-scoped public routes (mount at /api/campaigns) ---
|
// --- Campaign-scoped public routes (mount at /api/campaigns) ---
|
||||||
const campaignPublicRouter = (0, express_1.Router)();
|
const campaignPublicRouter = (0, express_1.Router)();
|
||||||
exports.responseCampaignPublicRouter = campaignPublicRouter;
|
exports.responseCampaignPublicRouter = campaignPublicRouter;
|
||||||
@ -111,7 +110,7 @@ responsesPublicRouter.get('/:id/report/:token', async (req, res, next) => {
|
|||||||
const responsesAdminRouter = (0, express_1.Router)();
|
const responsesAdminRouter = (0, express_1.Router)();
|
||||||
exports.responsesAdminRouter = responsesAdminRouter;
|
exports.responsesAdminRouter = responsesAdminRouter;
|
||||||
responsesAdminRouter.use(auth_middleware_1.authenticate);
|
responsesAdminRouter.use(auth_middleware_1.authenticate);
|
||||||
responsesAdminRouter.use((0, rbac_middleware_1.requireRole)(...ADMIN_ROLES));
|
responsesAdminRouter.use((0, rbac_middleware_1.requireRole)(...roles_1.INFLUENCE_ROLES));
|
||||||
// GET /api/responses
|
// GET /api/responses
|
||||||
responsesAdminRouter.get('/', (0, validate_1.validate)(responses_schemas_1.listAdminResponsesSchema, 'query'), async (req, res, next) => {
|
responsesAdminRouter.get('/', (0, validate_1.validate)(responses_schemas_1.listAdminResponsesSchema, 'query'), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -82,11 +82,11 @@ export declare const listAdminResponsesSchema: z.ZodObject<{
|
|||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
limit: number;
|
limit: number;
|
||||||
page: number;
|
page: number;
|
||||||
status?: "PENDING" | "APPROVED" | "REJECTED" | undefined;
|
status?: "APPROVED" | "PENDING" | "REJECTED" | undefined;
|
||||||
search?: string | undefined;
|
search?: string | undefined;
|
||||||
campaignId?: string | undefined;
|
campaignId?: string | undefined;
|
||||||
}, {
|
}, {
|
||||||
status?: "PENDING" | "APPROVED" | "REJECTED" | undefined;
|
status?: "APPROVED" | "PENDING" | "REJECTED" | undefined;
|
||||||
search?: string | undefined;
|
search?: string | undefined;
|
||||||
limit?: number | undefined;
|
limit?: number | undefined;
|
||||||
page?: number | undefined;
|
page?: number | undefined;
|
||||||
@ -99,9 +99,9 @@ export declare const updateResponseStatusSchema: z.ZodObject<{
|
|||||||
REJECTED: "REJECTED";
|
REJECTED: "REJECTED";
|
||||||
}>;
|
}>;
|
||||||
}, "strip", z.ZodTypeAny, {
|
}, "strip", z.ZodTypeAny, {
|
||||||
status: "PENDING" | "APPROVED" | "REJECTED";
|
status: "APPROVED" | "PENDING" | "REJECTED";
|
||||||
}, {
|
}, {
|
||||||
status: "PENDING" | "APPROVED" | "REJECTED";
|
status: "APPROVED" | "PENDING" | "REJECTED";
|
||||||
}>;
|
}>;
|
||||||
export type SubmitResponseInput = z.infer<typeof submitResponseSchema>;
|
export type SubmitResponseInput = z.infer<typeof submitResponseSchema>;
|
||||||
export type ListPublicResponsesInput = z.infer<typeof listPublicResponsesSchema>;
|
export type ListPublicResponsesInput = z.infer<typeof listPublicResponsesSchema>;
|
||||||
|
|||||||
@ -4,14 +4,14 @@ exports.updateResponseStatusSchema = exports.listAdminResponsesSchema = exports.
|
|||||||
const zod_1 = require("zod");
|
const zod_1 = require("zod");
|
||||||
const client_1 = require("@prisma/client");
|
const client_1 = require("@prisma/client");
|
||||||
exports.submitResponseSchema = zod_1.z.object({
|
exports.submitResponseSchema = zod_1.z.object({
|
||||||
representativeName: zod_1.z.string().min(1, 'Representative name is required'),
|
representativeName: zod_1.z.string().min(1, 'Representative name is required').max(200),
|
||||||
representativeLevel: zod_1.z.nativeEnum(client_1.GovernmentLevel),
|
representativeLevel: zod_1.z.nativeEnum(client_1.GovernmentLevel),
|
||||||
responseType: zod_1.z.nativeEnum(client_1.ResponseType),
|
responseType: zod_1.z.nativeEnum(client_1.ResponseType),
|
||||||
responseText: zod_1.z.string().min(1, 'Response text is required'),
|
responseText: zod_1.z.string().min(1, 'Response text is required').max(5000),
|
||||||
representativeTitle: zod_1.z.string().optional(),
|
representativeTitle: zod_1.z.string().max(200).optional(),
|
||||||
representativeEmail: zod_1.z.string().email().optional(),
|
representativeEmail: zod_1.z.string().email().optional(),
|
||||||
userComment: zod_1.z.string().optional(),
|
userComment: zod_1.z.string().max(1000).optional(),
|
||||||
submittedByName: zod_1.z.string().optional(),
|
submittedByName: zod_1.z.string().max(200).optional(),
|
||||||
submittedByEmail: zod_1.z.string().email().optional(),
|
submittedByEmail: zod_1.z.string().email().optional(),
|
||||||
isAnonymous: zod_1.z.boolean().optional().default(false),
|
isAnonymous: zod_1.z.boolean().optional().default(false),
|
||||||
sendVerification: zod_1.z.boolean().optional().default(false),
|
sendVerification: zod_1.z.boolean().optional().default(false),
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"responses.schemas.js","sourceRoot":"","sources":["../../../../src/modules/influence/responses/responses.schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,2CAA+E;AAElE,QAAA,oBAAoB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC3C,kBAAkB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,iCAAiC,CAAC;IACxE,mBAAmB,EAAE,OAAC,CAAC,UAAU,CAAC,wBAAe,CAAC;IAClD,YAAY,EAAE,OAAC,CAAC,UAAU,CAAC,qBAAY,CAAC;IACxC,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,2BAA2B,CAAC;IAC5D,mBAAmB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC1C,mBAAmB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IAClD,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,eAAe,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACtC,gBAAgB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IAC/C,WAAW,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAClD,gBAAgB,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CACxD,CAAC,CAAC;AAEU,QAAA,yBAAyB,GAAG,OAAC,CAAC,MAAM,CAAC;IAChD,IAAI,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,IAAI,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;IAC5E,KAAK,EAAE,OAAC,CAAC,UAAU,CAAC,wBAAe,CAAC,CAAC,QAAQ,EAAE;CAChD,CAAC,CAAC;AAEU,QAAA,wBAAwB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC/C,IAAI,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,MAAM,EAAE,OAAC,CAAC,UAAU,CAAC,uBAAc,CAAC,CAAC,QAAQ,EAAE;IAC/C,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC;AAEU,QAAA,0BAA0B,GAAG,OAAC,CAAC,MAAM,CAAC;IACjD,MAAM,EAAE,OAAC,CAAC,UAAU,CAAC,uBAAc,CAAC;CACrC,CAAC,CAAC"}
|
{"version":3,"file":"responses.schemas.js","sourceRoot":"","sources":["../../../../src/modules/influence/responses/responses.schemas.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,2CAA+E;AAElE,QAAA,oBAAoB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC3C,kBAAkB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,iCAAiC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IACjF,mBAAmB,EAAE,OAAC,CAAC,UAAU,CAAC,wBAAe,CAAC;IAClD,YAAY,EAAE,OAAC,CAAC,UAAU,CAAC,qBAAY,CAAC;IACxC,YAAY,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,2BAA2B,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;IACtE,mBAAmB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IACnD,mBAAmB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IAClD,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;IAC5C,eAAe,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IAC/C,gBAAgB,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IAC/C,WAAW,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAClD,gBAAgB,EAAE,OAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;CACxD,CAAC,CAAC;AAEU,QAAA,yBAAyB,GAAG,OAAC,CAAC,MAAM,CAAC;IAChD,IAAI,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,IAAI,EAAE,OAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;IAC5E,KAAK,EAAE,OAAC,CAAC,UAAU,CAAC,wBAAe,CAAC,CAAC,QAAQ,EAAE;CAChD,CAAC,CAAC;AAEU,QAAA,wBAAwB,GAAG,OAAC,CAAC,MAAM,CAAC;IAC/C,IAAI,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACnD,KAAK,EAAE,OAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,MAAM,EAAE,OAAC,CAAC,UAAU,CAAC,uBAAc,CAAC,CAAC,QAAQ,EAAE;IAC/C,UAAU,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACjC,MAAM,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC9B,CAAC,CAAC;AAEU,QAAA,0BAA0B,GAAG,OAAC,CAAC,MAAM,CAAC;IACjD,MAAM,EAAE,OAAC,CAAC,UAAU,CAAC,uBAAc,CAAC;CACrC,CAAC,CAAC"}
|
||||||
@ -100,8 +100,9 @@ export declare const responsesService: {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
upvoteCount: number;
|
upvoteCount: number;
|
||||||
campaignSlug: string;
|
submittedByUserId: string | null;
|
||||||
campaignId: string;
|
campaignId: string;
|
||||||
|
campaignSlug: string;
|
||||||
representativeName: string;
|
representativeName: string;
|
||||||
representativeTitle: string | null;
|
representativeTitle: string | null;
|
||||||
representativeLevel: import(".prisma/client").$Enums.GovernmentLevel;
|
representativeLevel: import(".prisma/client").$Enums.GovernmentLevel;
|
||||||
@ -119,7 +120,6 @@ export declare const responsesService: {
|
|||||||
verifiedAt: Date | null;
|
verifiedAt: Date | null;
|
||||||
verifiedBy: string | null;
|
verifiedBy: string | null;
|
||||||
submittedIp: string | null;
|
submittedIp: string | null;
|
||||||
submittedByUserId: string | null;
|
|
||||||
}>;
|
}>;
|
||||||
deleteResponse(id: string): Promise<void>;
|
deleteResponse(id: string): Promise<void>;
|
||||||
resendVerification(id: string): Promise<{
|
resendVerification(id: string): Promise<{
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"responses.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/responses/responses.service.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EACvB,yBAAyB,EAC1B,MAAM,qBAAqB,CAAC;AAI7B,eAAO,MAAM,gBAAgB;yBAGA,MAAM,QAAQ,mBAAmB,aAAa,MAAM;;;;;uBAmEtD,MAAM,WAAW,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;mBA0D7C,MAAM;;;;;;uBA2BF,MAAM,WAAW,MAAM,WAAW,MAAM;;;;;;;6BAgClC,MAAM,WAAW,MAAM,WAAW,MAAM;;;uBAwB9C,MAAM,SAAS,MAAM;;;;;;;;;uBAqCrB,MAAM,SAAS,MAAM;;;;;;;;;qBA6BvB,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAwDvB,MAAM,QAAQ,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAUrC,MAAM;2BAOF,MAAM;;;CA6CpC,CAAC"}
|
{"version":3,"file":"responses.service.d.ts","sourceRoot":"","sources":["../../../../src/modules/influence/responses/responses.service.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EACvB,yBAAyB,EAC1B,MAAM,qBAAqB,CAAC;AAI7B,eAAO,MAAM,gBAAgB;yBAGA,MAAM,QAAQ,mBAAmB,aAAa,MAAM;;;;;uBA8FtD,MAAM,WAAW,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;mBA0D7C,MAAM;;;;;;uBA2BF,MAAM,WAAW,MAAM,WAAW,MAAM;;;;;;;6BAgClC,MAAM,WAAW,MAAM,WAAW,MAAM;;;uBAwB9C,MAAM,SAAS,MAAM;;;;;;;;;uBAqCrB,MAAM,SAAS,MAAM;;;;;;;;;qBA6BvB,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAwDvB,MAAM,QAAQ,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAUrC,MAAM;2BAOF,MAAM;;;CA6CpC,CAAC"}
|
||||||
@ -6,8 +6,12 @@ const client_1 = require("@prisma/client");
|
|||||||
const database_1 = require("../../../config/database");
|
const database_1 = require("../../../config/database");
|
||||||
const error_handler_1 = require("../../../middleware/error-handler");
|
const error_handler_1 = require("../../../middleware/error-handler");
|
||||||
const email_service_1 = require("../../../services/email.service");
|
const email_service_1 = require("../../../services/email.service");
|
||||||
|
const notification_queue_service_1 = require("../../../services/notification-queue.service");
|
||||||
|
const notification_helper_1 = require("../../../services/notification.helper");
|
||||||
const env_1 = require("../../../config/env");
|
const env_1 = require("../../../config/env");
|
||||||
|
const logger_1 = require("../../../utils/logger");
|
||||||
const metrics_1 = require("../../../utils/metrics");
|
const metrics_1 = require("../../../utils/metrics");
|
||||||
|
const rocketchat_webhook_service_1 = require("../../../services/rocketchat-webhook.service");
|
||||||
const VERIFICATION_EXPIRY_DAYS = 30;
|
const VERIFICATION_EXPIRY_DAYS = 30;
|
||||||
exports.responsesService = {
|
exports.responsesService = {
|
||||||
// --- Public ---
|
// --- Public ---
|
||||||
@ -64,6 +68,32 @@ exports.responsesService = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
(0, metrics_1.recordResponseSubmission)();
|
(0, metrics_1.recordResponseSubmission)();
|
||||||
|
// Notification: admin response submitted alert
|
||||||
|
try {
|
||||||
|
if (await (0, notification_helper_1.isNotificationEnabled)('notifyAdminResponseSubmitted')) {
|
||||||
|
const adminEmails = await (0, notification_helper_1.getAdminEmailsByRole)([client_1.UserRole.SUPER_ADMIN, client_1.UserRole.INFLUENCE_ADMIN]);
|
||||||
|
if (adminEmails.length > 0) {
|
||||||
|
const adminUrl = `${env_1.env.ADMIN_URL || 'http://localhost:3000'}/app/influence/responses`;
|
||||||
|
await notification_queue_service_1.notificationQueueService.enqueue({
|
||||||
|
type: 'admin-response-submitted',
|
||||||
|
adminEmails,
|
||||||
|
campaignTitle: campaign.title,
|
||||||
|
representativeName: data.representativeName,
|
||||||
|
responseType: data.responseType,
|
||||||
|
submitterName: data.isAnonymous ? 'Anonymous' : (data.submittedByName || 'Anonymous'),
|
||||||
|
adminUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
logger_1.logger.error('Failed to enqueue response submitted notification:', err);
|
||||||
|
}
|
||||||
|
// Notify Rocket.Chat
|
||||||
|
rocketchat_webhook_service_1.rocketchatWebhookService.onCampaignResponseSubmitted({
|
||||||
|
campaignTitle: campaign.title,
|
||||||
|
representativeName: data.representativeName,
|
||||||
|
}).catch(() => { });
|
||||||
return {
|
return {
|
||||||
id: response.id,
|
id: response.id,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
{"version":3,"file":"listmonk.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/listmonk/listmonk.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAgJxB,OAAO,EAAE,MAAM,IAAI,cAAc,EAAE,CAAC"}
|
{"version":3,"file":"listmonk.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/listmonk/listmonk.routes.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAwJxB,OAAO,EAAE,MAAM,IAAI,cAAc,EAAE,CAAC"}
|
||||||
9
api/dist/modules/listmonk/listmonk.routes.js
vendored
9
api/dist/modules/listmonk/listmonk.routes.js
vendored
@ -2,16 +2,17 @@
|
|||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.listmonkRouter = void 0;
|
exports.listmonkRouter = void 0;
|
||||||
const express_1 = require("express");
|
const express_1 = require("express");
|
||||||
const client_1 = require("@prisma/client");
|
|
||||||
const auth_middleware_1 = require("../../middleware/auth.middleware");
|
const auth_middleware_1 = require("../../middleware/auth.middleware");
|
||||||
const rbac_middleware_1 = require("../../middleware/rbac.middleware");
|
const rbac_middleware_1 = require("../../middleware/rbac.middleware");
|
||||||
const listmonk_client_1 = require("../../services/listmonk.client");
|
const listmonk_client_1 = require("../../services/listmonk.client");
|
||||||
const listmonk_sync_service_1 = require("../../services/listmonk-sync.service");
|
const listmonk_sync_service_1 = require("../../services/listmonk-sync.service");
|
||||||
|
const listmonk_event_sync_service_1 = require("../../services/listmonk-event-sync.service");
|
||||||
const env_1 = require("../../config/env");
|
const env_1 = require("../../config/env");
|
||||||
|
const roles_1 = require("../../utils/roles");
|
||||||
const router = (0, express_1.Router)();
|
const router = (0, express_1.Router)();
|
||||||
exports.listmonkRouter = router;
|
exports.listmonkRouter = router;
|
||||||
router.use(auth_middleware_1.authenticate);
|
router.use(auth_middleware_1.authenticate);
|
||||||
router.use((0, rbac_middleware_1.requireRole)(client_1.UserRole.SUPER_ADMIN));
|
router.use((0, rbac_middleware_1.requireRole)(...roles_1.BROADCAST_ROLES));
|
||||||
// GET /api/listmonk — sync status
|
// GET /api/listmonk — sync status
|
||||||
router.get('/', async (_req, res, next) => {
|
router.get('/', async (_req, res, next) => {
|
||||||
try {
|
try {
|
||||||
@ -119,6 +120,10 @@ router.post('/reinitialize', async (_req, res, next) => {
|
|||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// GET /api/listmonk/event-sync-stats — event-driven sync stats
|
||||||
|
router.get('/event-sync-stats', (_req, res) => {
|
||||||
|
res.json(listmonk_event_sync_service_1.listmonkEventSyncService.getStats());
|
||||||
|
});
|
||||||
// GET /api/listmonk/proxy-url — get proxy port + token for iframe embedding
|
// GET /api/listmonk/proxy-url — get proxy port + token for iframe embedding
|
||||||
router.get('/proxy-url', (req, res, _next) => {
|
router.get('/proxy-url', (req, res, _next) => {
|
||||||
const token = req.headers.authorization?.slice(7) || '';
|
const token = req.headers.authorization?.slice(7) || '';
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"listmonk.routes.js","sourceRoot":"","sources":["../../../src/modules/listmonk/listmonk.routes.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"listmonk.routes.js","sourceRoot":"","sources":["../../../src/modules/listmonk/listmonk.routes.ts"],"names":[],"mappings":";;;AAAA,qCAAkE;AAClE,sEAAgE;AAChE,sEAA+D;AAC/D,oEAAgE;AAChE,gFAA2E;AAC3E,4FAAsF;AACtF,0CAAuC;AACvC,6CAAoD;AAEpD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAwJL,gCAAc;AAvJjC,MAAM,CAAC,GAAG,CAAC,8BAAY,CAAC,CAAC;AACzB,MAAM,CAAC,GAAG,CAAC,IAAA,6BAAW,EAAC,GAAG,uBAAe,CAAC,CAAC,CAAC;AAE5C,kCAAkC;AAClC,MAAM,CAAC,GAAG,CACR,GAAG,EACH,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,2CAAmB,CAAC,SAAS,EAAE,CAAC;QAC/C,wBAAwB;QACxB,MAAM,SAAS,GAAG,MAAM,gCAAc,CAAC,WAAW,EAAE,CAAC;QACrD,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,mDAAmD;AACnD,MAAM,CAAC,GAAG,CACR,QAAQ,EACR,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,2CAAmB,CAAC,QAAQ,EAAE,CAAC;QACnD,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,qCAAqC;AACrC,MAAM,CAAC,IAAI,CACT,kBAAkB,EAClB,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,gCAAc,CAAC,WAAW,EAAE,CAAC;QACnD,GAAG,CAAC,IAAI,CAAC;YACP,OAAO,EAAE,OAAO;YAChB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,2BAA2B;SACzE,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,uCAAuC;AACvC,MAAM,CAAC,IAAI,CACT,oBAAoB,EACpB,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,2CAAmB,CAAC,wBAAwB,EAAE,CAAC;QACrE,2CAAmB,CAAC,aAAa,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QAC9C,GAAG,CAAC,IAAI,CAAC;YACP,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,UAAU,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,wBAAwB;YAC3E,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,2CAAmB,CAAC,YAAY,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACnF,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,oCAAoC;AACpC,MAAM,CAAC,IAAI,CACT,iBAAiB,EACjB,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,2CAAmB,CAAC,aAAa,EAAE,CAAC;QAC1D,2CAAmB,CAAC,aAAa,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QAC9C,GAAG,CAAC,IAAI,CAAC;YACP,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,UAAU,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,YAAY;YAC/D,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,2CAAmB,CAAC,YAAY,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACnF,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,gCAAgC;AAChC,MAAM,CAAC,IAAI,CACT,aAAa,EACb,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,2CAAmB,CAAC,SAAS,EAAE,CAAC;QACtD,2CAAmB,CAAC,aAAa,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QAC9C,GAAG,CAAC,IAAI,CAAC;YACP,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,UAAU,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,QAAQ;YAC3D,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,2CAAmB,CAAC,YAAY,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACnF,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,8BAA8B;AAC9B,MAAM,CAAC,IAAI,CACT,WAAW,EACX,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,2CAAmB,CAAC,OAAO,EAAE,CAAC;QACpD,GAAG,CAAC,IAAI,CAAC;YACP,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,qBAAqB;YAC9B,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,kCAAkC;AAClC,MAAM,CAAC,IAAI,CACT,eAAe,EACf,KAAK,EAAE,IAAa,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;IACzD,IAAI,CAAC;QACH,MAAM,2CAAmB,CAAC,YAAY,EAAE,CAAC;QACzC,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAC;IAC9D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,CAAC,CAAC;IACZ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,+DAA+D;AAC/D,MAAM,CAAC,GAAG,CACR,mBAAmB,EACnB,CAAC,IAAa,EAAE,GAAa,EAAE,EAAE;IAC/B,GAAG,CAAC,IAAI,CAAC,sDAAwB,CAAC,QAAQ,EAAE,CAAC,CAAC;AAChD,CAAC,CACF,CAAC;AAEF,4EAA4E;AAC5E,MAAM,CAAC,GAAG,CACR,YAAY,EACZ,CAAC,GAAY,EAAE,GAAa,EAAE,KAAmB,EAAE,EAAE;IACnD,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAG,CAAC,mBAAmB,EAAE,KAAK,EAAE,CAAC,CAAC;AACrD,CAAC,CACF,CAAC"}
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"canvass.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/canvass/canvass.routes.ts"],"names":[],"mappings":"AAyBA,QAAA,MAAM,eAAe,4CAAW,CAAC;AAgQjC,QAAA,MAAM,WAAW,4CAAW,CAAC;AAsF7B,OAAO,EAAE,eAAe,IAAI,sBAAsB,EAAE,WAAW,IAAI,kBAAkB,EAAE,CAAC"}
|
{"version":3,"file":"canvass.routes.d.ts","sourceRoot":"","sources":["../../../../src/modules/map/canvass/canvass.routes.ts"],"names":[],"mappings":"AAyBA,QAAA,MAAM,eAAe,4CAAW,CAAC;AAgQjC,QAAA,MAAM,WAAW,4CAAW,CAAC;AAoG7B,OAAO,EAAE,eAAe,IAAI,sBAAsB,EAAE,WAAW,IAAI,kBAAkB,EAAE,CAAC"}
|
||||||
20
api/dist/modules/map/canvass/canvass.routes.js
vendored
20
api/dist/modules/map/canvass/canvass.routes.js
vendored
@ -12,7 +12,7 @@ const validate_1 = require("../../../middleware/validate");
|
|||||||
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
const auth_middleware_1 = require("../../../middleware/auth.middleware");
|
||||||
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
const rbac_middleware_1 = require("../../../middleware/rbac.middleware");
|
||||||
const rate_limit_1 = require("../../../middleware/rate-limit");
|
const rate_limit_1 = require("../../../middleware/rate-limit");
|
||||||
const MAP_ADMIN_ROLES = [client_1.UserRole.SUPER_ADMIN, client_1.UserRole.MAP_ADMIN];
|
const roles_1 = require("../../../utils/roles");
|
||||||
// ─── Volunteer Router ────────────────────────────────────────────────
|
// ─── Volunteer Router ────────────────────────────────────────────────
|
||||||
const volunteerRouter = (0, express_1.Router)();
|
const volunteerRouter = (0, express_1.Router)();
|
||||||
exports.canvassVolunteerRouter = volunteerRouter;
|
exports.canvassVolunteerRouter = volunteerRouter;
|
||||||
@ -138,17 +138,17 @@ volunteerRouter.put('/locations/:id', (0, validate_1.validate)(canvass_schemas_1
|
|||||||
// POST /api/map/canvass/locations — create a new location (role-gated fields)
|
// POST /api/map/canvass/locations — create a new location (role-gated fields)
|
||||||
volunteerRouter.post('/locations', (0, validate_1.validate)(canvass_schemas_1.volunteerCreateLocationSchema), async (req, res, next) => {
|
volunteerRouter.post('/locations', (0, validate_1.validate)(canvass_schemas_1.volunteerCreateLocationSchema), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const role = req.user.role;
|
|
||||||
const data = { ...req.body };
|
const data = { ...req.body };
|
||||||
// Strip fields based on role
|
// Strip fields based on role
|
||||||
const isAdmin = role === client_1.UserRole.SUPER_ADMIN || role === client_1.UserRole.MAP_ADMIN;
|
const userRoles = req.user.roles || [req.user.role];
|
||||||
|
const isAdmin = userRoles.some((r) => r === client_1.UserRole.SUPER_ADMIN || r === client_1.UserRole.MAP_ADMIN);
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
delete data.firstName;
|
delete data.firstName;
|
||||||
delete data.lastName;
|
delete data.lastName;
|
||||||
delete data.email;
|
delete data.email;
|
||||||
delete data.phone;
|
delete data.phone;
|
||||||
}
|
}
|
||||||
if (role === client_1.UserRole.TEMP) {
|
if (userRoles.length === 1 && userRoles[0] === client_1.UserRole.TEMP) {
|
||||||
delete data.supportLevel;
|
delete data.supportLevel;
|
||||||
delete data.sign;
|
delete data.sign;
|
||||||
delete data.signSize;
|
delete data.signSize;
|
||||||
@ -210,7 +210,7 @@ volunteerRouter.post('/visits/bulk', rate_limit_1.canvassBulkVisitRateLimit, //
|
|||||||
const adminRouter = (0, express_1.Router)();
|
const adminRouter = (0, express_1.Router)();
|
||||||
exports.canvassAdminRouter = adminRouter;
|
exports.canvassAdminRouter = adminRouter;
|
||||||
adminRouter.use(auth_middleware_1.authenticate);
|
adminRouter.use(auth_middleware_1.authenticate);
|
||||||
adminRouter.use((0, rbac_middleware_1.requireRole)(...MAP_ADMIN_ROLES));
|
adminRouter.use((0, rbac_middleware_1.requireRole)(...roles_1.MAP_ROLES));
|
||||||
// GET /api/map/canvass/stats
|
// GET /api/map/canvass/stats
|
||||||
adminRouter.get('/stats', async (_req, res, next) => {
|
adminRouter.get('/stats', async (_req, res, next) => {
|
||||||
try {
|
try {
|
||||||
@ -273,4 +273,14 @@ adminRouter.get('/visits', (0, validate_1.validate)(canvass_schemas_1.adminVisit
|
|||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// GET /api/map/canvass/trends
|
||||||
|
adminRouter.get('/trends', (0, validate_1.validate)(canvass_schemas_1.outcomeTrendsQuerySchema, 'query'), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const result = await canvass_service_1.canvassService.getOutcomeTrends(req.query);
|
||||||
|
res.json(result);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
//# sourceMappingURL=canvass.routes.js.map
|
//# sourceMappingURL=canvass.routes.js.map
|
||||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user