Repository: Supernova3339/changerawr Branch: master Commit: a72f1c9e9084 Files: 624 Total size: 5.8 MB Directory structure: gitextract_pand8s4o/ ├── .dockerignore ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.yml │ │ └── feature-request.yml │ └── workflows/ │ ├── build-platform.yml │ └── docker-build.yml ├── .gitignore ├── .idea/ │ ├── .gitignore │ ├── changerawr.iml │ ├── dataSources.xml │ ├── data_source_mapping.xml │ ├── db-forest-config.xml │ ├── discord.xml │ ├── inspectionProfiles/ │ │ └── Project_Default.xml │ ├── jsLibraryMappings.xml │ ├── modules.xml │ ├── sqldialects.xml │ └── vcs.xml ├── APIDOCGUIDE.md ├── CHANGELOG.md ├── Caddyfile ├── Dockerfile ├── Dockerfile.compose ├── LICENSE ├── PROJECT_INDEX.md ├── README.md ├── app/ │ ├── (auth)/ │ │ ├── forgot-password/ │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── login/ │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── oauth-callback/ │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── register/ │ │ │ └── [token]/ │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── reset-password/ │ │ │ └── [token]/ │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── reset-password-form.tsx │ │ ├── setup/ │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── two-factor/ │ │ ├── layout.tsx │ │ └── page.tsx │ ├── (email)/ │ │ └── unsubscribed/ │ │ └── page.tsx │ ├── .well-known/ │ │ └── acme-challenge/ │ │ ├── [token]/ │ │ │ ├── route.ts │ │ │ └── route.ts.disabled │ │ └── route.ts │ ├── api/ │ │ ├── acme/ │ │ │ ├── cancel/ │ │ │ │ └── [certId]/ │ │ │ │ └── route.ts │ │ │ ├── issue/ │ │ │ │ └── route.ts │ │ │ ├── renew/ │ │ │ │ └── [certId]/ │ │ │ │ └── route.ts │ │ │ ├── revoke/ │ │ │ │ └── [certId]/ │ │ │ │ └── route.ts │ │ │ ├── status/ │ │ │ │ └── [certId]/ │ │ │ │ └── route.ts │ │ │ └── verify-dns/ │ │ │ └── route.ts │ │ ├── admin/ │ │ │ ├── ai-settings/ │ │ │ │ ├── route.ts │ │ │ │ └── test-key/ │ │ │ │ └── route.ts │ │ │ ├── analytics/ │ │ │ │ └── route.ts │ │ │ ├── api-keys/ │ │ │ │ ├── [keyId]/ │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── audit-logs/ │ │ │ │ ├── actions/ │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── config/ │ │ │ │ ├── route.ts │ │ │ │ └── system-email/ │ │ │ │ └── route.ts │ │ │ ├── dashboard/ │ │ │ │ └── route.ts │ │ │ ├── oauth/ │ │ │ │ └── providers/ │ │ │ │ ├── [id]/ │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── saml/ │ │ │ │ └── providers/ │ │ │ │ ├── [id]/ │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── sponsor/ │ │ │ │ └── route.ts │ │ │ ├── system/ │ │ │ │ └── slack/ │ │ │ │ └── route.ts │ │ │ └── users/ │ │ │ ├── [userId]/ │ │ │ │ ├── role/ │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── invitations/ │ │ │ │ ├── [id]/ │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── ai/ │ │ │ ├── decrypt/ │ │ │ │ └── route.ts │ │ │ └── settings/ │ │ │ └── route.ts │ │ ├── analytics/ │ │ │ └── track/ │ │ │ └── route.ts │ │ ├── auth/ │ │ │ ├── change-password/ │ │ │ │ └── route.ts │ │ │ ├── cli/ │ │ │ │ ├── generate/ │ │ │ │ │ └── route.ts │ │ │ │ ├── refresh/ │ │ │ │ │ └── route.ts │ │ │ │ └── token/ │ │ │ │ └── route.ts │ │ │ ├── connections/ │ │ │ │ └── route.ts │ │ │ ├── forgot-password/ │ │ │ │ └── route.ts │ │ │ ├── invitation/ │ │ │ │ └── [token]/ │ │ │ │ └── route.ts │ │ │ ├── login/ │ │ │ │ ├── route.ts │ │ │ │ └── second-factor/ │ │ │ │ └── route.ts │ │ │ ├── logout/ │ │ │ │ └── route.ts │ │ │ ├── me/ │ │ │ │ └── route.ts │ │ │ ├── oauth/ │ │ │ │ ├── authorize/ │ │ │ │ │ └── [providerName]/ │ │ │ │ │ └── route.ts │ │ │ │ ├── callback/ │ │ │ │ │ └── [providerName]/ │ │ │ │ │ └── route.ts │ │ │ │ └── providers/ │ │ │ │ └── route.ts │ │ │ ├── passkeys/ │ │ │ │ ├── [id]/ │ │ │ │ │ └── route.ts │ │ │ │ ├── authenticate/ │ │ │ │ │ ├── options/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── verify/ │ │ │ │ │ └── route.ts │ │ │ │ ├── register/ │ │ │ │ │ ├── options/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── verify/ │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── preview/ │ │ │ │ └── route.ts │ │ │ ├── refresh/ │ │ │ │ └── route.ts │ │ │ ├── register/ │ │ │ │ └── route.ts │ │ │ ├── reset-password/ │ │ │ │ ├── [token]/ │ │ │ │ │ └── route.ts │ │ │ │ └── request/ │ │ │ │ └── route.ts │ │ │ ├── saml/ │ │ │ │ ├── authorize/ │ │ │ │ │ └── [providerName]/ │ │ │ │ │ └── route.ts │ │ │ │ ├── callback/ │ │ │ │ │ └── [providerName]/ │ │ │ │ │ └── route.ts │ │ │ │ ├── metadata/ │ │ │ │ │ └── [providerName]/ │ │ │ │ │ └── route.ts │ │ │ │ └── providers/ │ │ │ │ └── route.ts │ │ │ ├── security-settings/ │ │ │ │ └── route.ts │ │ │ ├── settings/ │ │ │ │ └── route.ts │ │ │ └── validate/ │ │ │ └── route.ts │ │ ├── avatar/ │ │ │ └── [hash]/ │ │ │ └── route.ts │ │ ├── changelog/ │ │ │ ├── [projectId]/ │ │ │ │ └── entries/ │ │ │ │ ├── all/ │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── entries/ │ │ │ │ └── [entryId]/ │ │ │ │ └── route.ts │ │ │ ├── requests/ │ │ │ │ ├── [requestId]/ │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── subscribe/ │ │ │ │ └── route.ts │ │ │ ├── unsubscribe/ │ │ │ │ └── [token]/ │ │ │ │ └── route.ts │ │ │ └── verify-domain/ │ │ │ └── route.ts │ │ ├── check-setup/ │ │ │ └── route.ts │ │ ├── config/ │ │ │ ├── runtime/ │ │ │ │ └── route.ts │ │ │ └── timezone/ │ │ │ └── route.ts │ │ ├── cron/ │ │ │ └── ssl-renewal/ │ │ │ └── route.ts │ │ ├── custom-domains/ │ │ │ ├── [domain]/ │ │ │ │ ├── browser-rules/ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── dns-instructions/ │ │ │ │ │ └── route.ts │ │ │ │ ├── route.ts │ │ │ │ ├── ssl/ │ │ │ │ │ ├── mode/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── revoke/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── toggle-https/ │ │ │ │ │ └── route.ts │ │ │ │ └── throttle/ │ │ │ │ └── route.ts │ │ │ ├── add/ │ │ │ │ └── route.ts │ │ │ ├── list/ │ │ │ │ └── route.ts │ │ │ └── verify/ │ │ │ └── route.ts │ │ ├── dashboard/ │ │ │ └── stats/ │ │ │ └── route.ts │ │ ├── domain-check/ │ │ │ └── route.ts │ │ ├── health/ │ │ │ └── route.ts │ │ ├── integrations/ │ │ │ ├── slack/ │ │ │ │ ├── callback/ │ │ │ │ │ └── route.ts │ │ │ │ └── manifest/ │ │ │ │ └── route.ts │ │ │ └── widget/ │ │ │ └── [projectId]/ │ │ │ ├── [widgetId]/ │ │ │ │ └── route.ts │ │ │ ├── list/ │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── internal/ │ │ │ ├── agent/ │ │ │ │ └── version/ │ │ │ │ └── route.ts │ │ │ ├── cert/ │ │ │ │ └── [domain]/ │ │ │ │ └── route.ts │ │ │ └── ip-config/ │ │ │ └── route.ts │ │ ├── projects/ │ │ │ ├── [projectId]/ │ │ │ │ ├── analytics/ │ │ │ │ │ ├── export/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── api-keys/ │ │ │ │ │ ├── [keyId]/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── catch-up/ │ │ │ │ │ ├── ai-summary/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── changelog/ │ │ │ │ │ ├── [entryId]/ │ │ │ │ │ │ ├── route.ts │ │ │ │ │ │ └── schedule/ │ │ │ │ │ │ ├── approval/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── route.ts │ │ │ │ │ └── tags/ │ │ │ │ │ ├── [tagId]/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── cli/ │ │ │ │ │ ├── link/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── sync/ │ │ │ │ │ │ ├── route.ts │ │ │ │ │ │ └── status/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── unlink/ │ │ │ │ │ └── route.ts │ │ │ │ ├── integrations/ │ │ │ │ │ ├── email/ │ │ │ │ │ │ ├── route.ts │ │ │ │ │ │ ├── send/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── test/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── github/ │ │ │ │ │ │ ├── generate/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ ├── route.ts │ │ │ │ │ │ ├── tags/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── test/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── slack/ │ │ │ │ │ ├── channels/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ ├── route.ts │ │ │ │ ├── settings/ │ │ │ │ │ └── route.ts │ │ │ │ └── versions/ │ │ │ │ └── route.ts │ │ │ ├── import/ │ │ │ │ ├── canny/ │ │ │ │ │ ├── fetch/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── validate/ │ │ │ │ │ └── route.ts │ │ │ │ ├── parse/ │ │ │ │ │ └── route.ts │ │ │ │ └── process/ │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── requests/ │ │ │ └── route.ts │ │ ├── search/ │ │ │ └── route.ts │ │ ├── setup/ │ │ │ ├── admin/ │ │ │ │ └── route.ts │ │ │ ├── invitations/ │ │ │ │ └── route.ts │ │ │ ├── oauth/ │ │ │ │ ├── auto/ │ │ │ │ │ └── route.ts │ │ │ │ ├── debug/ │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ ├── settings/ │ │ │ │ └── route.ts │ │ │ ├── setupProgressSchema.ts │ │ │ ├── status/ │ │ │ │ └── route.ts │ │ │ └── types.ts │ │ ├── subscribers/ │ │ │ ├── [subscriberId]/ │ │ │ │ └── route.ts │ │ │ ├── generate-mock/ │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── system/ │ │ │ ├── agent-version/ │ │ │ │ └── route.ts │ │ │ ├── easypanel/ │ │ │ │ └── status/ │ │ │ │ └── route.ts │ │ │ ├── perform-update/ │ │ │ │ └── route.ts │ │ │ ├── update-status/ │ │ │ │ └── route.ts │ │ │ └── version/ │ │ │ └── route.ts │ │ └── telemetry/ │ │ ├── config/ │ │ │ └── route.ts │ │ └── debug/ │ │ └── route.ts │ ├── api-docs/ │ │ └── route.ts │ ├── changelog/ │ │ ├── [projectId]/ │ │ │ ├── [entryId]/ │ │ │ │ └── page.tsx │ │ │ ├── changelog-view.tsx │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ ├── not-found.tsx │ │ │ ├── page.tsx │ │ │ └── rss.xml/ │ │ │ └── route.ts │ │ └── custom-domain/ │ │ └── [domain]/ │ │ ├── [entryId]/ │ │ │ ├── EntryContent.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── rss.xml/ │ │ │ └── route.ts │ │ └── unsubscribed/ │ │ └── page.tsx │ ├── cli/ │ │ └── auth/ │ │ └── page.tsx │ ├── dashboard/ │ │ ├── admin/ │ │ │ ├── about/ │ │ │ │ └── page.tsx │ │ │ ├── ai-settings/ │ │ │ │ └── page.tsx │ │ │ ├── analytics/ │ │ │ │ └── page.tsx │ │ │ ├── api-keys/ │ │ │ │ └── page.tsx │ │ │ ├── audit-logs/ │ │ │ │ └── page.tsx │ │ │ ├── domains/ │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── oauth/ │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── requests/ │ │ │ │ └── page.tsx │ │ │ ├── system/ │ │ │ │ ├── email/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── slack/ │ │ │ │ │ └── page.tsx │ │ │ │ └── templates/ │ │ │ │ └── page.tsx │ │ │ └── users/ │ │ │ └── page.tsx │ │ ├── bookmarks/ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── projects/ │ │ │ ├── [projectId]/ │ │ │ │ ├── analytics/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── api-keys/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── catch-up/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── changelog/ │ │ │ │ │ ├── [entryId]/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── new/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── domains/ │ │ │ │ │ ├── [domain]/ │ │ │ │ │ │ ├── client.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── DomainCard.tsx │ │ │ │ │ │ ├── InlineSSLSetup.tsx │ │ │ │ │ │ ├── SSLCertificateCard.tsx │ │ │ │ │ │ ├── SSLCertificateWizard.tsx │ │ │ │ │ │ ├── SSLManagement.tsx │ │ │ │ │ │ └── ssl/ │ │ │ │ │ │ ├── ExternalSSLManagement.tsx │ │ │ │ │ │ ├── SSLCertificateActions.tsx │ │ │ │ │ │ ├── SSLCertificateStatus.tsx │ │ │ │ │ │ ├── SSLDNSInstructions.tsx │ │ │ │ │ │ ├── SSLModeSelector.tsx │ │ │ │ │ │ ├── SSLVerificationMethod.tsx │ │ │ │ │ │ ├── SSLVerificationProgress.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── page.tsx.backup │ │ │ │ ├── import/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── integrations/ │ │ │ │ │ ├── email/ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ └── subscribers/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── github/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── slack/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── widget/ │ │ │ │ │ ├── [widgetId]/ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ └── widget-editor.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── widget-config.tsx │ │ │ │ │ └── widget-list.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── settings/ │ │ │ │ └── page.tsx │ │ │ ├── new/ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── providers.tsx │ │ ├── requests/ │ │ │ └── page.tsx │ │ └── settings/ │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ └── startup.ts ├── components/ │ ├── CommandPalette.tsx │ ├── DinoGame.tsx │ ├── Logo.tsx │ ├── MarkdownEditor.tsx │ ├── UpdateIndicatorBadge.tsx │ ├── UpdateStatus.tsx │ ├── admin/ │ │ ├── api/ │ │ │ ├── PermissionsModal.tsx │ │ │ ├── Rename.tsx │ │ │ ├── sdk-showcase-compact.tsx │ │ │ └── sdk-showcase.tsx │ │ ├── audit-logs/ │ │ │ └── VirtualizedList.tsx │ │ └── requests/ │ │ └── Management.tsx │ ├── analytics/ │ │ ├── analytics-chart.tsx │ │ ├── analytics-metric-card.tsx │ │ ├── country-analytics-table.tsx │ │ ├── entry-analytics-table.tsx │ │ ├── project-analytics-table.tsx │ │ └── referrer-analytics-table.tsx │ ├── changelog/ │ │ ├── ButtonGroup.tsx │ │ ├── ChangelogActionRequest.tsx │ │ ├── ChangelogEditor.tsx │ │ ├── ChangelogEntries.tsx │ │ ├── RequestHandler.tsx │ │ ├── ScrollToTopButton.tsx │ │ ├── ShareButton.tsx │ │ ├── ThemeToggle.tsx │ │ ├── WidgetPreview.tsx │ │ └── editor/ │ │ ├── AITitleGenerator.tsx │ │ ├── EditorHeader.tsx │ │ ├── TagColorPicker.tsx │ │ ├── TagSelector.tsx │ │ ├── TagSuggester.tsx │ │ ├── VersionSelector.tsx │ │ └── scheduler/ │ │ └── ScheduleEntryDialog.tsx │ ├── dashboard/ │ │ └── WhatsNewModal.tsx │ ├── github/ │ │ ├── GitHubGenerateDialog.tsx │ │ └── GitHubIntegrationSettings.tsx │ ├── loading-spinner.tsx │ ├── markdown-editor/ │ │ ├── MarkdownEditor.tsx │ │ ├── MarkdownEditorArea.tsx │ │ ├── MarkdownPreview.tsx │ │ ├── MarkdownToolbar.tsx │ │ ├── RenderMarkdown.tsx │ │ ├── StatusBar.tsx │ │ ├── ai/ │ │ │ └── AIAssistantPanel.tsx │ │ ├── hooks/ │ │ │ └── useCUMModals.ts │ │ ├── index.ts │ │ ├── modals/ │ │ │ ├── CUMAlertModal.tsx │ │ │ ├── CUMButtonModal.tsx │ │ │ ├── CUMEmbedModal.tsx │ │ │ ├── CUMTableModal.tsx │ │ │ └── index.ts │ │ ├── types/ │ │ │ └── cum-extensions.ts │ │ └── utils/ │ │ ├── formatting.ts │ │ ├── keyboard-shortcuts.ts │ │ ├── renderer.ts │ │ └── safe-keyboard-shortcuts.tsx │ ├── project/ │ │ ├── ProjectNavItem.tsx │ │ ├── ProjectSettingsPage.tsx │ │ ├── ProjectSidebar.tsx │ │ ├── RecentChangelogItem.tsx │ │ ├── catch-up/ │ │ │ ├── CatchUpEntry.tsx │ │ │ ├── CatchUpView.tsx │ │ │ └── SinceSelector.tsx │ │ └── settings/ │ │ └── TagManagement.tsx │ ├── projects/ │ │ └── importing/ │ │ ├── ChangelogImportModal.tsx │ │ ├── ImportDataPrompt.tsx │ │ └── integrations/ │ │ └── CannyImportStep.tsx │ ├── providers/ │ │ ├── CommandPaletteProvider.tsx │ │ └── setup-context.tsx │ ├── settings/ │ │ ├── connected-sso-section.tsx │ │ ├── passkeys-section.tsx │ │ └── security-settings.tsx │ ├── setup/ │ │ ├── TeamImportModal.tsx │ │ ├── setup-context.tsx │ │ ├── setup-step.tsx │ │ └── steps/ │ │ ├── admin-step.tsx │ │ ├── completion-step.tsx │ │ ├── oauth-step.tsx │ │ ├── settings-step.tsx │ │ ├── team-step.tsx │ │ └── welcome-step.tsx │ ├── sso/ │ │ └── ProviderLogo.tsx │ ├── subscription-form.tsx │ ├── telemetry/ │ │ └── PromptModal.tsx │ ├── theme-provider.tsx │ └── ui/ │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── breadcrumb.tsx │ ├── breadcrumbs.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── confetti.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── error-alert.tsx │ ├── form.tsx │ ├── hover-card.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── radio-group.tsx │ ├── scroll-area.tsx │ ├── searchable-select.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── sidebar.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ └── tooltip.tsx ├── components.json ├── context/ │ └── auth.tsx ├── docker-compose-online.yml ├── docker-compose.yml ├── docker-entrypoint-compose.sh ├── docker-entrypoint.sh ├── emails/ │ ├── approval-notification.tsx │ ├── changelog.tsx │ ├── password-reset.tsx │ ├── rejection-notification.tsx │ └── schedule-published.tsx ├── eslint.config.mjs ├── hooks/ │ ├── use-local-storage.ts │ ├── use-media-query.ts │ ├── use-timezone.ts │ ├── use-toast.ts │ ├── useAIAssistant.ts │ ├── useBookmarks.ts │ ├── useChunkedData.ts │ ├── useCommandPalette.ts │ ├── useEditorHistory.ts │ ├── useMarkdownState.ts │ ├── useSlashCommands.ts │ ├── useTelemetry.ts │ └── useWhatsNew.ts ├── ideas.md ├── instrumentation.ts ├── issues.md ├── jsdoc.json ├── lib/ │ ├── api/ │ │ ├── README.md │ │ ├── middleware.ts │ │ ├── permissions.ts │ │ └── route-permissions.ts │ ├── app-info.ts │ ├── auth/ │ │ ├── api-key.ts │ │ ├── authorization.ts │ │ ├── claim-validator.ts │ │ ├── cli-auth.ts │ │ ├── email-domain-validator.ts │ │ ├── jwt-utils.ts │ │ ├── oauth.ts │ │ ├── password.ts │ │ ├── providers/ │ │ │ ├── easypanel/ │ │ │ │ ├── auto-setup.ts │ │ │ │ └── client.ts │ │ │ ├── easypanel.ts │ │ │ └── pocketid.ts │ │ ├── saml.ts │ │ ├── tokens.ts │ │ └── webauthn.ts │ ├── constants/ │ │ └── timezones.ts │ ├── custom-domains/ │ │ ├── constants.ts │ │ ├── dns.ts │ │ ├── service.ts │ │ ├── ssl/ │ │ │ ├── acme-account.ts │ │ │ ├── auto-renewal.ts │ │ │ ├── cron-setup.md │ │ │ ├── encryption.ts │ │ │ ├── is-supported.ts │ │ │ ├── service.ts │ │ │ ├── setup-renewal-job.ts │ │ │ ├── ssrf-guard.ts │ │ │ └── webhook.ts │ │ ├── utils.ts │ │ └── validation.ts │ ├── db.ts │ ├── middleware/ │ │ └── analytics.ts │ ├── services/ │ │ ├── analytics/ │ │ │ └── geolocation.ts │ │ ├── auth/ │ │ │ ├── password-breach.ts │ │ │ └── password-reset.ts │ │ ├── bookmarks/ │ │ │ └── bookmark.service.ts │ │ ├── changelog/ │ │ │ └── rss.ts │ │ ├── core/ │ │ │ ├── markdown/ │ │ │ │ ├── EXTENSIONS.md │ │ │ │ ├── EXTENSIONS_SETUP.md │ │ │ │ ├── extensions/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── subtext/ │ │ │ │ │ │ └── subtext.ts │ │ │ │ │ └── table/ │ │ │ │ │ └── table.ts │ │ │ │ └── useCustomExtensions.ts │ │ │ └── system-user/ │ │ │ ├── README.md │ │ │ └── service.ts │ │ ├── easypanel/ │ │ │ └── index.ts │ │ ├── email/ │ │ │ ├── notification.ts │ │ │ └── schedule-notification.ts │ │ ├── github/ │ │ │ ├── changelog-generator.ts │ │ │ └── client.ts │ │ ├── jobs/ │ │ │ ├── executors/ │ │ │ │ ├── changelog-publish.executor.ts │ │ │ │ ├── ssl-renewal.executor.ts │ │ │ │ └── telemetry-send.executor.ts │ │ │ ├── job-runner.service.ts │ │ │ └── scheduled-job.service.ts │ │ ├── projects/ │ │ │ ├── catch-up/ │ │ │ │ └── catch-up.service.ts │ │ │ └── importing/ │ │ │ ├── index.ts │ │ │ ├── integrations/ │ │ │ │ └── canny.service.ts │ │ │ ├── markdown-parser.service.ts │ │ │ ├── processor.service.ts │ │ │ └── validation.service.ts │ │ ├── request/ │ │ │ └── changelog-request.ts │ │ ├── search/ │ │ │ └── service.ts │ │ ├── slack/ │ │ │ ├── index.ts │ │ │ ├── logo.tsx │ │ │ └── post-message.ts │ │ ├── sponsor/ │ │ │ └── service.ts │ │ └── telemetry/ │ │ └── service.ts │ ├── types/ │ │ ├── analytics.ts │ │ ├── auth.ts │ │ ├── changelog.ts │ │ ├── cli/ │ │ │ └── project-api.ts │ │ ├── custom-domains.ts │ │ ├── dashboard.ts │ │ ├── easypanel.ts │ │ ├── oauth.ts │ │ ├── projects/ │ │ │ ├── catch-up/ │ │ │ │ └── types.ts │ │ │ ├── importing/ │ │ │ │ └── canny.ts │ │ │ └── importing.ts │ │ ├── saml.ts │ │ ├── settings.ts │ │ ├── sso.ts │ │ └── telemetry.ts │ ├── utils/ │ │ ├── ai/ │ │ │ ├── prompts.ts │ │ │ ├── secton.ts │ │ │ └── types.ts │ │ ├── analytics.ts │ │ ├── api.ts │ │ ├── auditLog.ts │ │ ├── changelog.ts │ │ ├── cookies.ts │ │ ├── docker.ts │ │ ├── encryption.ts │ │ ├── format-date.ts │ │ ├── gravatar.ts │ │ ├── rate-limit.ts │ │ └── text.ts │ └── utils.ts ├── next.config.ts ├── next.openapi.json ├── nginx.conf ├── package-lock.json.backup ├── package.json ├── package.json.backup ├── postcss.config.mjs ├── prisma/ │ ├── migrations/ │ │ ├── 20250215042251_init_authentication/ │ │ │ └── migration.sql │ │ ├── 20250215051621_add_invitation_links/ │ │ │ └── migration.sql │ │ ├── 20250215063030_add_last_logged_in_at_field/ │ │ │ └── migration.sql │ │ ├── 20250215075317_add_changelog_models/ │ │ │ └── migration.sql │ │ ├── 20250215080528_add_viewer_role/ │ │ │ └── migration.sql │ │ ├── 20250215180005_add_project_settings/ │ │ │ └── migration.sql │ │ ├── 20250215232640_add_api_keys/ │ │ │ └── migration.sql │ │ ├── 20250216005356_add_audit_logs/ │ │ │ └── migration.sql │ │ ├── 20250216050812_add_system_configuration/ │ │ │ └── migration.sql │ │ ├── 20250216053424_update_changelog_requests_schema/ │ │ │ └── migration.sql │ │ ├── 20250221000121_add_new_request_types/ │ │ │ └── migration.sql │ │ ├── 20250305225249_add_oauth/ │ │ │ └── migration.sql │ │ ├── 20250418044737_add_email_integration/ │ │ │ └── migration.sql │ │ ├── 20250418045834_add_email_logs/ │ │ │ └── migration.sql │ │ ├── 20250418161805_add_email_log_enums/ │ │ │ └── migration.sql │ │ ├── 20250418163601_add_email_subscription/ │ │ │ └── migration.sql │ │ ├── 20250419000001_add_password_reset/ │ │ │ └── migration.sql │ │ ├── 20250421000001_add_notification_preferences/ │ │ │ └── migration.sql │ │ ├── 20250504203021_passkey_support/ │ │ │ └── migration.sql │ │ ├── 20250504203707_add_last_challenge/ │ │ │ └── migration.sql │ │ ├── 20250504211358_add_two_factor_mode/ │ │ │ └── migration.sql │ │ ├── 20250504212400_add_two_factor_session/ │ │ │ └── migration.sql │ │ ├── 20250511204818_add_ai_assistant_config/ │ │ │ └── migration.sql │ │ ├── 20250512000000_add_github_integration/ │ │ │ └── migration.sql │ │ ├── 20250610120000_fix_user_deletion_constraints/ │ │ │ └── migration.sql │ │ ├── 20250613040144_changelog_analytics/ │ │ │ └── migration.sql │ │ ├── 20250624024525_add_custom_domains/ │ │ │ └── migration.sql │ │ ├── 20250625030954_add_custom_domains_to_email_notifications/ │ │ │ └── migration.sql │ │ ├── 20250625120000_add_changelog_scheduling/ │ │ │ └── migration.sql │ │ ├── 20250627020135_add_color_field_to_changelog_tag/ │ │ │ └── migration.sql │ │ ├── 20250628171029_add_telemetry/ │ │ │ └── migration.sql │ │ ├── 20250628175000_preemptive_telemetry_setup/ │ │ │ └── migration.sql │ │ ├── 20250628180800_add_telemetry_support/ │ │ │ └── migration.sql │ │ ├── 20250628215000_complete_telemetry_fix/ │ │ │ └── migration.sql │ │ ├── 20250628215100_fix_telemetry_casting/ │ │ │ └── migration.sql │ │ ├── 20250628230000_add_changelogentryid_back/ │ │ │ └── migration.sql │ │ ├── 20250702092443_add_cli_auth_codes/ │ │ │ └── migration.sql │ │ ├── 20250702121802_add_synced_cli_migrations_and_metadata/ │ │ │ └── migration.sql │ │ ├── 20250703092000_fix_missing_changelogentryid_column/ │ │ │ └── migration.sql │ │ ├── 20251031_add_metadata_to_changelog_request/ │ │ │ └── migration.sql │ │ ├── 20251111210147_add_api_key_and_widgets/ │ │ │ └── migration.sql │ │ ├── 20251127011835_add_slack_integration/ │ │ │ └── migration.sql │ │ ├── 20251127015242_add_slack_oauth_to_system_config/ │ │ │ └── migration.sql │ │ ├── 20251127_add_slack_signing_secret/ │ │ │ └── migration.sql │ │ ├── 20260219214321_add_license_fields/ │ │ │ └── migration.sql │ │ ├── 20260219215954_fix_changelog_request_cascade_delete/ │ │ │ └── migration.sql │ │ ├── 20260220120000_add_timezone_config/ │ │ │ └── migration.sql │ │ ├── 20260220140000_add_custom_date_templates/ │ │ │ └── migration.sql │ │ └── migration_lock.toml │ ├── schema/ │ │ ├── README.md │ │ ├── base.prisma │ │ ├── enums.prisma │ │ ├── integrations.prisma │ │ ├── projects.prisma │ │ ├── system.prisma │ │ └── users.prisma │ ├── schema.prisma.backup │ └── seed.ts ├── proxy.ts ├── public/ │ └── swagger.json ├── scripts/ │ ├── api/ │ │ ├── generateSwagger.js │ │ └── generateSwagger.ts │ ├── ftb/ │ │ ├── index.html │ │ └── server.js │ ├── maintenance/ │ │ ├── index.html │ │ └── server.js │ ├── nginx-reload.sh │ ├── utils/ │ │ └── scan.ts │ └── widget/ │ └── build.ts ├── tailwind.config.ts ├── temp_migration.sql ├── tsconfig.json ├── useful-information-for-development/ │ └── slack_scopes.md └── widgets/ ├── changelog/ │ └── index.js ├── core/ │ └── styles/ │ ├── announcement.css │ ├── common.css │ ├── floating.css │ ├── modal.css │ ├── reset.css │ └── variables.css └── variants/ ├── announcement.js ├── classic.js ├── floating.js └── modal.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ .git .github .next node_modules .env .env.* *.log README.md .DS_Store npm-debug.log* yarn-debug.log* yarn-error.log* .idea .vscode .eslintcache coverage dist .turbo public/swagger.json public/widget-bundle.js public/widget-bundle.js.map useful-information-for-development APIDOCGUIDE.md CHANGELOG.md docker-compose-online.yml docker-compose.yml ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: supernova3339 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: superdevofficial # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.yml ================================================ name: 🐛 Bug Report description: Found a bug? Help us squash it! Every bug report makes Changerawr better. title: "[Bug]: " labels: ["bug", "triage"] assignees: - Supernova3339 body: - type: markdown attributes: value: | # 🐛 Thanks for reporting a bug! Bug reports are super valuable - they help us make Changerawr rock solid for everyone. The more details you provide, the faster we can track down and fix the issue. **Please search existing issues first** to see if this bug has already been reported. If you find it, give it a 👍! - type: input id: contact attributes: label: 📧 Contact Details (Optional) description: How can we reach you if we need more info about this bug? placeholder: email@example.com or @username validations: required: false - type: textarea id: bug-description attributes: label: 🐛 What happened? description: Describe the bug clearly and concisely placeholder: Tell us what went wrong... validations: required: true - type: textarea id: expected-behavior attributes: label: ✅ What should have happened? description: What did you expect to happen instead? placeholder: Describe what you thought would happen... validations: required: true - type: textarea id: reproduction-steps attributes: label: 🔄 Steps to Reproduce description: How can we reproduce this bug? Be as specific as possible! placeholder: | 1. Go to '...' 2. Click on '...' 3. Scroll down to '...' 4. See error value: | 1. 2. 3. 4. validations: required: true - type: dropdown id: frequency attributes: label: 📊 How often does this happen? description: Is this bug consistent or intermittent? options: - "Every time (100%)" - "Most of the time (75%+)" - "Sometimes (25-75%)" - "Rarely (less than 25%)" - "Only happened once" default: 0 validations: required: true - type: dropdown id: browser attributes: label: 🌐 Browser description: Which browser are you using? multiple: true options: - Chrome - Firefox - Safari - Microsoft Edge - Opera - Other - type: dropdown id: device-type attributes: label: 📱 Device Type description: What device were you using? options: - Desktop (Windows) - Desktop (macOS) - Desktop (Linux) - Mobile (iOS) - Mobile (Android) - Tablet (iOS) - Tablet (Android) default: 0 validations: required: true - type: input id: screen-resolution attributes: label: 🖥️ Screen Resolution (Optional) description: What's your screen resolution? Helpful for UI bugs placeholder: "e.g., 1920x1080, 1366x768" validations: required: false - type: dropdown id: changerawr-area attributes: label: 🎯 Which part of Changerawr? description: Where in the app did this bug occur? options: - Dashboard / Home - Changelog Editor - Project Settings - User Settings / Profile - Authentication / Login - Embeddable Widget - API / Integrations - Email Notifications - GitHub Integration - Mobile Interface - Deployment/Pre-Deployment - Other / Not Sure default: 10 validations: required: true - type: dropdown id: severity attributes: label: ⚡ Bug Severity description: How much does this impact your use of Changerawr? options: - "Low - Minor annoyance, doesn't block my work" - "Medium - Inconvenient but I can work around it" - "High - Significantly impacts my workflow" - "Critical - Completely blocks me from using Changerawr" default: 0 validations: required: true - type: textarea id: error-messages attributes: label: ❌ Error Messages description: Any error messages, console errors, or warnings you saw? placeholder: Copy and paste any error messages here... render: text validations: required: false - type: textarea id: console-logs attributes: label: 🔍 Console Output (For Developers) description: If you're comfortable with dev tools, paste any relevant console output placeholder: Right-click → Inspect → Console tab → copy any red errors render: shell validations: required: false - type: textarea id: additional-context attributes: label: 📝 Additional Context description: Anything else that might be relevant? Screenshots, network conditions, etc. placeholder: Any other details that might help us understand this bug... validations: required: false - type: input id: changerawr-version attributes: label: 🦖 Changerawr Version (Optional) description: If you know the version, it helps! Check the about page in administration settings, or your lib/app-info.ts placeholder: "e.g., v0.3.9" validations: required: false - type: checkboxes id: bug-checklist attributes: label: 📋 Bug Report Checklist description: Quick checks to help us process your report options: - label: I've searched existing issues and this isn't a duplicate required: true - label: I can reproduce this bug consistently required: false - label: I've included steps to reproduce the issue required: true - label: I'd be willing to test a fix if provided required: false - label: I am able to provide a reproduction video in the event that I can not provide reproduction steps ( youtube, loom, etc ) required: true - label: I have all required ENV variables configured. required: true - label: I am using docker ( only check this if you are deploying with docker! ) required: false - label: I am not using Cloudflare Zero Trust ( it breaks everything! ) required: true - type: markdown attributes: value: | --- ## 🛠️ What happens next? 1. **We'll investigate** - Usually within 24-72 hours 2. **Reproduce the bug** - We'll try to recreate the issue 3. **Ask questions** - We might need more details 4. **Fix it** - Once confirmed, we'll work on a solution 5. **Test & Release** - Fix gets tested and deployed **Pro tip**: The best bug reports include clear steps to reproduce the issue. If we can reproduce it, we can fix it! Thanks for helping make Changerawr more stable! 🦖💪 ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.yml ================================================ name: 🦖 Feature Request description: Got an idea to make Changerawr even better? We'd love to hear it! title: "[Feature]: " labels: ["enhancement", "feature-request"] assignees: - Supernova3339 body: - type: markdown attributes: value: | # 🎉 Thanks for helping make Changerawr better! We built Changerawr because existing tools didn't cut it. Your ideas help us build exactly what developers and teams actually need. **Please search existing issues first** to avoid duplicates. If you find a similar request, give it a 👍 instead! - type: input id: contact attributes: label: 📧 Contact Details (Optional) description: How can we reach you if we need to discuss this feature? placeholder: email@example.com or @username validations: required: false - type: textarea id: feature-description attributes: label: 🚀 Feature Description description: What feature would you like to see added to Changerawr? placeholder: Be as detailed as you can! What should this feature do? validations: required: true - type: textarea id: problem-solving attributes: label: 🎯 Problem This Solves description: What problem or pain point would this feature address? placeholder: Describe the current limitation or frustration this would solve validations: required: true - type: textarea id: use-case attributes: label: 📝 Use Case / User Story description: How would you (or others) use this feature? placeholder: "As a [type of user], I want [feature] so that [benefit]" validations: required: true - type: dropdown id: user-type attributes: label: 👤 What best describes you? description: This helps us understand our user base and prioritize features options: - Solo Developer - Small Team (2-10 people) - Medium Team (11-50 people) - Large Team (50+ people) - Open Source Project Maintainer - Product Manager - Other default: 0 validations: required: true - type: dropdown id: priority attributes: label: ⚡ How important is this to you? description: Help us understand the impact this would have options: - "Nice to have - would be cool but not urgent" - "Useful - would improve my workflow" - "Important - would solve a real problem I face" - "Critical - blocking me from using Changerawr effectively" default: 0 validations: required: true - type: textarea id: alternatives attributes: label: 🔄 Current Workarounds description: How are you currently handling this without the feature? placeholder: Describe any workarounds you're using or tools you're missing validations: required: false - type: textarea id: implementation-ideas attributes: label: 💡 Implementation Ideas (Optional) description: Any thoughts on how this could work? UI mockups, API endpoints, etc. placeholder: Don't worry about technical details - but if you have ideas, we'd love to hear them! validations: required: false - type: dropdown id: feature-area attributes: label: 🎨 What area of Changerawr is this for? description: Which part of the platform would this feature affect? multiple: true options: - Dashboard / UI - Content Editor - API / Integrations - Embeddable Widget - Email Notifications - Authentication / Users - GitHub Integration - Analytics / Reporting - Mobile Experience - Performance - Other / Not Sure - type: checkboxes id: additional-info attributes: label: 📋 Additional Information description: A few quick checks to help us out options: - label: I've searched existing issues and this isn't a duplicate required: true - label: I'd be willing to test this feature if implemented required: false - label: I'd be interested in contributing to this feature's development required: false - type: markdown attributes: value: | --- ## 🙏 What happens next? 1. **We'll review your request** - Usually within a few days 2. **Community feedback** - Others can vote with 👍 and add comments 3. **Discussion** - We might ask follow-up questions 4. **Prioritization** - Popular requests with clear use cases get priority 5. **Development** - If accepted, we'll add it to our roadmap **Remember**: Changerawr is built by developers for developers. The best feature requests come from real problems you're facing! Thanks for helping make Changerawr rawrsome! 🦖 ================================================ FILE: .github/workflows/build-platform.yml ================================================ name: Build Platform Image on: workflow_call: inputs: platform: required: true type: string description: 'Platform to build (linux/amd64 or linux/arm64)' image_name: required: true type: string description: 'Image name' version: required: true type: string description: 'Version label for the Docker image tag' no_cache: required: false type: boolean default: false description: 'Disable Docker layer cache' env: REGISTRY: ghcr.io jobs: build: runs-on: ubuntu-latest timeout-minutes: 45 permissions: contents: read packages: write actions: write steps: - name: Checkout (HEAD of current branch) uses: actions/checkout@v4 - name: Set up QEMU (for ARM builds) if: inputs.platform == 'linux/arm64' uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set platform suffix id: platform run: | PLATFORM_SUFFIX=$(echo "${{ inputs.platform }}" | sed 's|linux/||') echo "suffix=$PLATFORM_SUFFIX" >> $GITHUB_OUTPUT - name: Delete GHA Docker cache (force rebuild) if: inputs.no_cache env: GH_TOKEN: ${{ github.token }} run: | echo "🗑️ Deleting cached Docker layers for scope: ${{ steps.platform.outputs.suffix }}" gh cache list --repo ${{ github.repository }} --json id,key \ | jq -r '.[] | select(.key | contains("${{ steps.platform.outputs.suffix }}")) | .id' \ | xargs -I{} gh cache delete {} --repo ${{ github.repository }} \ || echo "No cache entries found or deletion skipped" - name: Build and push uses: docker/build-push-action@v5 with: context: . platforms: ${{ inputs.platform }} push: true no-cache: ${{ inputs.no_cache }} build-args: | CACHEBUST=${{ inputs.no_cache && github.run_id || '1' }} tags: ${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ inputs.version }}-${{ steps.platform.outputs.suffix }} cache-from: ${{ inputs.no_cache == false && format('type=gha,scope={0}', steps.platform.outputs.suffix) || '' }} cache-to: ${{ inputs.no_cache == false && format('type=gha,mode=max,scope={0}', steps.platform.outputs.suffix) || '' }} labels: | org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.version=${{ inputs.version }} - name: Build complete run: | echo "✅ Built ${{ inputs.platform }} image: ${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ inputs.version }}-${{ steps.platform.outputs.suffix }}" ================================================ FILE: .github/workflows/docker-build.yml ================================================ name: Build and Push Docker Image on: push: tags: - 'v*' workflow_dispatch: inputs: version: description: 'Version to build (e.g., v1.0.4)' required: true type: string force_rebuild: description: 'Force rebuild (ignore cache)' required: false type: boolean default: false env: REGISTRY: ghcr.io jobs: info: runs-on: ubuntu-latest outputs: image_name: ${{ steps.meta.outputs.image_name }} version: ${{ steps.meta.outputs.version }} steps: - name: Show build info id: meta run: | IMAGE_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') if [ -n "${{ github.event.inputs.version }}" ]; then VERSION="${{ github.event.inputs.version }}" TRIGGER="Manual (workflow_dispatch)" else VERSION="${{ github.ref_name }}" TRIGGER="Tag push" fi echo "🚀 Docker Build Starting" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "📦 Repository: ${{ github.repository }}" echo "🏷️ Version: $VERSION" echo "🎯 Trigger: $TRIGGER" echo "🐳 Registry: ${{ env.REGISTRY }}" echo "" echo "📋 Will be tagged as:" echo " • ${{ env.REGISTRY }}/$IMAGE_NAME:$VERSION" echo " • ${{ env.REGISTRY }}/$IMAGE_NAME:latest" echo "" echo "🏗️ Platforms: linux/amd64, linux/arm64" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "image_name=$IMAGE_NAME" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT build-amd64: needs: info uses: ./.github/workflows/build-platform.yml with: platform: 'linux/amd64' image_name: ${{ needs.info.outputs.image_name }} version: ${{ needs.info.outputs.version }} no_cache: ${{ inputs.force_rebuild || false }} build-arm64: needs: info uses: ./.github/workflows/build-platform.yml with: platform: 'linux/arm64' image_name: ${{ needs.info.outputs.image_name }} version: ${{ needs.info.outputs.version }} no_cache: ${{ inputs.force_rebuild || false }} create-manifest: needs: [info, build-amd64, build-arm64] runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read packages: write steps: - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create and push multi-arch manifest run: | docker buildx imagetools create \ -t ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:${{ needs.info.outputs.version }} \ ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:${{ needs.info.outputs.version }}-amd64 \ ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:${{ needs.info.outputs.version }}-arm64 docker buildx imagetools create \ -t ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:latest \ ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:${{ needs.info.outputs.version }}-amd64 \ ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:${{ needs.info.outputs.version }}-arm64 - name: Summary run: | echo "## 🚀 Docker Image Built Successfully!" >> $GITHUB_STEP_SUMMARY echo "**Image:** \`${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}\`" >> $GITHUB_STEP_SUMMARY echo "**Version:** \`${{ needs.info.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY echo "**Tags:** \`${{ needs.info.outputs.version }}\`, \`latest\`" >> $GITHUB_STEP_SUMMARY echo "**Platforms:** linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Pull:**" >> $GITHUB_STEP_SUMMARY echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY echo "docker pull ${{ env.REGISTRY }}/${{ needs.info.outputs.image_name }}:${{ needs.info.outputs.version }}" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. .idea/dbnavigator.xml # dependencies /node_modules /.pnp .pnp.* .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/versions # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) .env .api-keys-for-development.sensitive.donotcommitthisever.env.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts # mistral experiments /app/api/experiments/mistral-ai/chat/route.ts /app/experiments/mistral-ai/page.tsx # widget build files /public/widgets /public/widget-announcement.js /public/widget-bundle.js /public/widget-classic.js /public/widget-floating.js /public/widget-modal.js # mapping files ( generated ) /public/widget-announcement.js.map /public/widget-bundle.js.map /public/widget-classic.js.map /public/widget-floating.js.map /public/widget-modal.js.map # sensitive changerawr shenanigans .changerawr .changerawr/donotdeletethisfolder/donotdeletethisfile.chrcli.json nul null .chtsredevmscli .claude # sponsorer server /sponsor-server # user requests to add CLAUDE.md ================================================ FILE: .idea/.gitignore ================================================ # Default ignored files /shelf/ /workspace.xml # Editor-based HTTP Client requests /httpRequests/ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml ================================================ FILE: .idea/changerawr.iml ================================================ ================================================ FILE: .idea/dataSources.xml ================================================ postgresql true org.postgresql.Driver jdbc:postgresql://localhost:5432/postgres $ProjectFileDir$ postgresql true org.postgresql.Driver jdbc:postgresql://localhost:5432/changerawr $ProjectFileDir$ ================================================ FILE: .idea/data_source_mapping.xml ================================================ ================================================ FILE: .idea/db-forest-config.xml ================================================ ================================================ FILE: .idea/discord.xml ================================================ ================================================ FILE: .idea/inspectionProfiles/Project_Default.xml ================================================ ================================================ FILE: .idea/jsLibraryMappings.xml ================================================ ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/sqldialects.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: APIDOCGUIDE.md ================================================ # API Documentation Generation Guide ## Overview When writing API documentation for routes, focus on creating clear, consistent, and comprehensive JSDoc comments that can be automatically parsed into OpenAPI/Swagger specifications. This guide outlines the required format and components for documenting API endpoints. ## Documentation Format Each route handler should be documented using JSDoc comments with specific tags that map to OpenAPI/Swagger fields. ### Basic Structure ```typescript /** * Brief description of the endpoint * @method HTTP_METHOD * @description Detailed description of what the endpoint does * @body Request body schema in JSON format * @response Status code and response schema * @error Error status codes and descriptions * @secure (optional) Security requirements */ ``` ## Required Tags ### @method Specifies the HTTP method. Must be one of: GET, POST, PUT, DELETE, PATCH ```typescript @method POST ``` ### @description Detailed description of the endpoint's functionality ```typescript @description Authenticates a user and returns JWT tokens for subsequent requests ``` ### @body (for POST/PUT/PATCH) JSON schema of the request body. Must be valid JSON format. ```typescript @body { "type": "object", "required": ["email", "password"], "properties": { "email": { "type": "string", "format": "email", "description": "User's email address" }, "password": { "type": "string", "minLength": 8, "description": "User's password" } } } ``` ### @response Response schema for successful requests. Include status code and JSON schema. ```typescript @response 200 { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "email": { "type": "string" } } } ``` ### @error Error responses with status codes and descriptions. ```typescript @error 400 Validation failed - Invalid input format @error 401 Unauthorized - Invalid credentials @error 404 Resource not found ``` ### @secure (Optional) Authentication requirements. Default is 'cookieAuth' for cookie-based authentication. ```typescript @secure bearerAuth ``` ## Examples ### GET Endpoint ```typescript /** * @method GET * @description Retrieves user profile information * @response 200 { * "type": "object", * "properties": { * "id": { "type": "string" }, * "name": { "type": "string" }, * "email": { "type": "string" }, * "role": { "type": "string" } * } * } * @error 401 Unauthorized - Please log in * @error 404 User not found * @secure cookieAuth */ export async function GET() { // Implementation } ``` ### POST Endpoint ```typescript /** * @method POST * @description Creates a new user account * @body { * "type": "object", * "required": ["email", "password", "name"], * "properties": { * "email": { * "type": "string", * "format": "email", * "description": "User's email address" * }, * "password": { * "type": "string", * "minLength": 8, * "description": "User's password" * }, * "name": { * "type": "string", * "description": "User's full name" * } * } * } * @response 201 { * "type": "object", * "properties": { * "id": { "type": "string" }, * "email": { "type": "string" }, * "name": { "type": "string" } * } * } * @error 400 Validation failed * @error 409 Email already exists */ export async function POST() { // Implementation } ``` ## Common Response Types ### Paginated Response ```typescript @response 200 { "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "title": { "type": "string" } } } }, "pagination": { "type": "object", "properties": { "total": { "type": "number" }, "page": { "type": "number" }, "pageSize": { "type": "number" }, "totalPages": { "type": "number" } } } } } ``` ### Error Response ```typescript @error 400 { "type": "object", "properties": { "error": { "type": "string" }, "details": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string" }, "message": { "type": "string" } } } } } } ``` ## Best Practices 1. **Consistency**: Use consistent naming and formatting across all endpoints 2. **Completeness**: Document all possible response codes and edge cases 3. **Validation**: Always include validation requirements in the schema 4. **Security**: Specify authentication requirements for protected routes 5. **Examples**: Provide examples for complex request/response schemas 6. **Descriptions**: Write clear, concise descriptions for all parameters ## Common Schemas Consider extracting common schemas to reduce duplication: ### User Schema ```typescript const UserSchema = { "type": "object", "properties": { "id": { "type": "string" }, "email": { "type": "string", "format": "email" }, "name": { "type": "string" }, "role": { "type": "string", "enum": ["user", "admin"] } } } ``` ### Pagination Schema ```typescript const PaginationSchema = { "type": "object", "properties": { "page": { "type": "number", "minimum": 1 }, "pageSize": { "type": "number", "minimum": 1 }, "total": { "type": "number" }, "totalPages": { "type": "number" } } } ``` Remember to validate your documentation against the OpenAPI specification to ensure compatibility with Swagger UI and other API documentation tools. ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. ## v1.0.6 - **Version 1.0.6 - 2026-04-16**: :::warning There are security vulnerabilities in previous releases, it is recommended to upgrade as soon as possible to this one. There are also a breaking change, set `SETUP_COMPLETE=true` in your .env file as this is how to complete the setup wizard now. I strongly recommend re-rolling all API keys. ::: ## Features - **Slack Integration** - You can now automatically post published entries to your Slack! - **Timezones** - Added timezone support per-user and globally, allowing for much more efficient collaboration between users with different time zones! - **SAML** - Adds support for SAML to SSO, adding a new method available for configuring single-sign-on. - *SSL Certificates for custom domains** - You can now get SSL certificates for custom domains! Just follow the nginx-sidecar documentation to get set up. - **IP Whitelisting** - You can now whitelist IP addresses to access the main panel. Note that this requires the nginx-sidecar to be set up. - **Version Templates** - Configure custom version format templates ## Improvements - **Extended Project Limits ** - Project limits have been extended for entries. If you wish to go beyond, please sponsor my work. - **UI Improvements** - Did various UI improvements to the administrative interface & fully redesigned the version picker - **Update Bar** - Whenever an update is available, you'll be much more likely to know thanks to the new pop-up. ## Bug Fixes - **Redirect Loop** - fixed issues with an infinite redirect loop after setup - **Strict Validation Local Domain** - Fixed issues with any domain being blocked for requests, preventing e.g., cloudflareinsghts from working, which subsequently blocked the panel from loading alltogether itself. - **System user show-up in admin dashboard** - Fixed an issue where the system user used for audit logs was being counted as a user in the total user count. ## Other - **Security Vulnerability patches** - A few vulnerabilities were disclosed to me, these have been patched in this release. - **Dependency Updates** - Updated to the latest dependencies, Nodemailer 8, and changerawr universal markdown 1.2.0! ## v1.0.5 - **Version 1.0.5 - 2025-11-13**: :::warning There are breaking changes in this release. ::: If you are using widgets at all, you will have to recreate them. If you are not using widgets, this does not affect you at all. --- ## Bug Fixes - **Email validation allowed for uppercase emails** - If you invited a email with any uppercase characters, you would not be able to login using it. This has been fixed! - **undefined showing up in the create SSO provider modal** - Sometimes, this would show up when you get a callback URL. This has been fixed. ### Features - **Catch Up!** - Missed too much while you were gone? Now you can catch up! `feature requested by my founder friends @ forento` - **Manually set when entry has been published** - I'll admit, I hated adding this request. Being transparent with your users is extremely important. However, it is now available for those who need it. Have fun! - **API Key Permissions + Project-Level API Keys** - Added a full permission system to API keys as well as making the ability to have them be project-level. - **Migrated from internal engine to package** - Migrated from the internal CUM engine to the package. Also adds support for Tables and discord-flavored SubText! - **Redid widgets entirely, proving four variants that can be customized to your hearts content!** ## Improvements - **Share on publish** - You can now share your changelog to your email subscribers when you publish an entry! - **Improved bookmarks usage** - Dedicated pages for bookmarking + QOL controls! - **You can now view changelog entries individually** - A dedicated page is available now for viewing a specific changelog entry. - **NextJS Upgrade** - Upgraded to NextJS 16! ## 1.0.4 - **Version 1.0.4 - 2025-?-?**: :::warning There are no breaking changes in this release. ::: ### Features - **Data Importing** - You can now import data from various sources to jump-start your Changerawr project! - **CUM** - Changerawr Universal Markdown engine introduces more functionality for the content editor.As of writing, Embeds, Buttons, and Alerts are available! Provides a better experience overall, improving the parsing engine, adding tokenization, and improving overall usability! ### Improvements - **Redesigned Projects Dashboard** - Updated the dashboard at (/dashboard/projects/projectId) to be less cluttered and more professional. - **Updated Projects Fetch Pagination** - Updated the default fetch for projects to 50 entries when listing. - **Redesigned Main Dashboard** - Redesigned the dashboard at (/dashboard), it needed a facelift and has been the same since 0.3.0 :( - **Content Editor Upgrades** - Introduces the CUM ( Changerawr Universal Markdown ) rendering engine for a better changelog experience. - **Improved Modal UI** - Added depth and optical borders to the modal UI. Also adds a "disableClose" prop for the rare case in which Changerawr is doing something and needs to let you know of its progress. hello world! ## 1.0.3 - **Version 1.0.3 - 2025-07-02**: **Features** - **Changerawr CLI** - Now it's easier then ever to get started with Changerawr! just do: ```shell npm install -g changerawr ``` You can find the source code at - would super appreciate a star! - Native [Pocket ID](https://pocket-id.org) Support. I really love [Pocket ID](https://pocket-id.org) so now you can configure it in three clicks from the installation wizard! **Bug Fixes** - **Migration Fixes** - Fixed an issue that broke deployment - **Setup Wizard Fixes** - Rewrote logic that broke older APIs, wizard will no longer skip steps, allowing you to finish setup completely. ## 1.0.2 - **Version 1.0.2 - 2025-06-24**: **Features** - **Custom Domains** - Added support for custom domains for a public changelog page. This was painful to implement :( - **Scheduled Publishing** - You can now schedule your changelog to be posted at a later date. - **Full-Text search** - Adds a command palette ( Ctrl + K / Cmd + K ) to search EVERYTHING :) - **Colored Tags** - Add color to your tags to make them pop! - **Telemetry** - Adds telemetry so the Changerawr team can collect anonymous usage statistics. **Improvements** - **UI Improvements** - Updated Request Management interface to provide more information to assist administrators with approving/denying a staff request. - **SSO Improvements** - Added a section to user settings so you can see what providers you have connected with! ## 1.0.1 - **Version 1.0.1 - 2025-06-22**: **Bug Fixes** - **Fixed LOGIN_ATTEMPT issue** - Thanks @aixxo ( on GitHub ) for reporting this! It has been fixed in this release. - **Security Vulnerability Patched** - AI assistant API key could be found without authenticating, this has been encrypted and fixed. Thanks to Codycody31 on GitHub for reporting this! You should set a brand new API key if you are using the AI integration. - **GitHub Integration scroll fix** - Fixed an issue that prevented you from seeing all of the content, which would get cut off and prevent interaction with the modal. ( you could not insert the content, as an example ) **Improvements** - **HIBP Integration** - When logging in, you will be notified if your password was found in a data breach. ## 1.0.0 - **Version 1.0.0 - 2025-06-15**: **Other** - **1.0.0 Release** - Thanks for your support of Changerawr! Have fun! ## 0.4.0 - **Version 0.4.0 - 2025-06-13**: **Features** - **Changelog Analytics** - Detailed analytics of your overall instance and projects! Cookieless, GDPR compliant, and 100% anonymized - **Easypanel Integration** - Changerawr can now update automagically if you use Easypanel for deployment! - **Automatic OAuth2 Setup** - If you use Easypanel OAuth2, you have the option to automatically setup OAuth2 for SSO if you have all the required variables set beforehand. **Bug Fixes** - **Bug Fixes** - A multitude of fixes and quirks that sometimes don't work correctly. Hopefully, this is every issue as of now fixed! **Improvements** - **Redesigned Components** - Redesigned some components ## 0.3.9 - **Version 0.3.9 - 2025-06-10**: **Features** - **Code Analyzation for GitHub Integration** - Changerawr can now look at your changed code and write a better changelog! **Bug Fixes** - **Account Deletion Fix** - Fixed an issue that prevented administrators from deleting a user's account. This process will anonymize all their data and actions while preserving their contributions. **Improvements** - **Redesigned the New Project page** - Gave a much-needed facelift to the new project page so it feels more roomy. Has confetti now, success messages, and more excitement! ## 0.3.8 - **Version 0.3.8 - 2025-06-10**: **Features** - **Invite Your Team!** - You can now invite your team early in the initial setup wizard, rawrsome! **Bug Fixes** - **Changelog Editor Fixes** - Fixed the version picker which would let you select versions already used, leading to issues. It will now check for overlapping before allowing you to set a version for your changelog. **Improvements** - **Component Redesigns** - Redesigned the Version picker and Editor header ## 0.3.7 - **Version 0.3.7 - 2025-06-09**: **Bug Fixes** - **Theme Switcher Fixes** - Fixed broken theme switcher logic, proper sync and no refreshing now! **Improvements** - **SSO Improvements** - You can now set all of the URLs for an SSO provider! ## 0.3.6 - **Version 0.3.6 - 2025-06-08**: **Bug Fixes** - **Fix Audit Log Issues** - Fixed any remaining bugs with audit logs. **Improvements** - **AI URL Change** - Changed some of the URLs the AI Assistant page links to. - **Improved Generation UI For GitHub** - Improved the generation UI and added more options ## 0.3.5 - **Version 0.3.5 - 2025-06-07**: **Features** - **GitHub integration** - You can now use GitHub with Changerawr! **Other** - **Routine Maintenance** - Package Updates and Bug Fixes have been triaged successfully with this release. ## 0.3.4 - **Version 0.3.4 - 2025-05-15**: **Features** - **Requests Manager** - Staff users can now view their permission requests! **Bug Fixes** - **TagAI Fixes** - Fixed TagAI to be more successful when recommending tags for a user's changelog entry ( e.g. fails less ) ## 0.3.3 - **Version 0.3.3 - 2025-05-13**: **Features** - **Title Generation** - AI can now analyze your writing to pick out the perfect title for your next changelog - **AI Assisted Tagging** - AI can now assist you with tagging your changelog - **Add New Tags** - You can now add a tag if it doesn't exist **Improvements** - **Better AI Assistant Design** - Redesigned AI Assistant ## 0.3.1 - **Version 0.3.1 - 2025-05-12**: **Features** - **New Feature** - Added "What's New" model to inform of latest changes from the most recent Changerawr update **Bug Fixes** - **Bug Fixes** - Fixed a few bugs with tokenization and the AI assistant ## 0.3.2 - **Version 0.3.2 - 2025-05-12**: **Features** - **'What's New' Modal Available on the About Page** - The "What's New" modal is now accessible from the About page. **Improvements** - **Redesigns** - Redesigned "What's New" modal. ## 0.3.0 - **Version 0.3.0 - 2025-05-11**: **Features** - **AI Assistant** - Adds an AI assistant to Changerawr! - **"What's New" model added** - Allow administrators to see what's new in Changerawr for the latest update! **Bug Fixes** - **Fixed Authentication Redirect** - Fixed authentication pages not redirecting once a session was obtained **Improvements** - **Redone Content Editor** - Redid the entire Content Editor from scratch ================================================ FILE: Caddyfile ================================================ # Caddyfile for Changerawr with on-demand TLS # Caddy automatically obtains and renews Let's Encrypt certificates { # Global options email {$ACME_EMAIL} # Enable on-demand TLS with ask endpoint on_demand_tls { ask http://app:3000/api/domain-check } } # Catch-all for custom domains :80, :443 { # Enable on-demand TLS for all incoming requests tls { on_demand } # Reverse proxy all requests to the Next.js app reverse_proxy app:3000 { # Pass original host header header_up Host {host} # Pass real IP and protocol header_up X-Real-IP {remote_host} header_up X-Forwarded-For {remote_host} header_up X-Forwarded-Proto {scheme} header_up X-Forwarded-Host {host} } # Enable compression encode gzip # Logging log { output stdout format console } } ================================================ FILE: Dockerfile ================================================ FROM node:22-alpine AS base # Install dependencies only when needed FROM base AS deps WORKDIR /app # Copy package files COPY package.json package-lock.json* ./ # CACHEBUST forces npm install to re-run even when package files are unchanged ARG CACHEBUST=1 RUN echo "Cache bust: $CACHEBUST" && npm install --legacy-peer-deps # Rebuild the source code only when needed FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules # Copy the entire project COPY . . # Generate Prisma client RUN npx prisma generate # Next.js collects completely anonymous telemetry data about general usage. # Learn more here: https://nextjs.org/telemetry # Uncomment the following line in case you want to disable telemetry during the build. ENV NEXT_TELEMETRY_DISABLED 1 ENV CI_BUILD_MODE 1 ENV DOCKER_BUILD 1 # Build the app RUN npm run build # Production image, copy all the files and run next FROM base AS runner WORKDIR /app ENV NODE_ENV production ENV NEXT_TELEMETRY_DISABLED 1 # Install all dependencies to satisfy entrypoint requirements COPY package.json package-lock.json* ./ RUN npm install --legacy-peer-deps # Install Prisma client with exact version match RUN npm uninstall prisma @prisma/client --legacy-peer-deps RUN npm install prisma@6.7.0 @prisma/client@6.7.0 --legacy-peer-deps # Install tsx explicitly RUN npm install -g tsx # Install esbuild for widget RUN npm install esbuild --legacy-peer-deps # Install JSDOC RUN npm install -g jsdoc # Add bash, nginx, and other dependencies for the entry script RUN apk add --no-cache bash wget nginx # Install nginx-agent from GitHub RUN wget -q https://github.com/Changerawr/nginx-agent/archive/refs/heads/master.tar.gz -O /tmp/nginx-agent.tar.gz && \ mkdir -p /nginx-agent && \ tar -xzf /tmp/nginx-agent.tar.gz -C /nginx-agent --strip-components=1 && \ rm /tmp/nginx-agent.tar.gz && \ cd /nginx-agent && \ npm install --production # Create nginx directories RUN mkdir -p /etc/nginx/sites-enabled /etc/nginx/sites-available /etc/ssl/changerawr /var/log/nginx /var/lib/nginx/tmp /run/nginx # Copy the entire project from the builder stage COPY --from=builder /app . # Copy maintenance page and server script COPY scripts/maintenance/index.html ./index.html COPY scripts/maintenance/server.js ./scripts/maintenance/server.js # Copy nginx configuration COPY nginx.conf /etc/nginx/nginx.conf # Copy and make nginx reload script executable COPY scripts/nginx-reload.sh /usr/local/bin/nginx-reload.sh RUN chmod +x /usr/local/bin/nginx-reload.sh # Ensure the entrypoint script is executable RUN chmod +x ./docker-entrypoint.sh EXPOSE 3000 80 443 ENV PORT 3000 ENV HOSTNAME "0.0.0.0" # Use entrypoint for running the build scripts before starting the server ENTRYPOINT ["/app/docker-entrypoint.sh"] CMD ["npm", "start"] ================================================ FILE: Dockerfile.compose ================================================ FROM node:20-alpine AS base # Install dependencies only when needed FROM base AS deps WORKDIR /app # Copy package files COPY package.json package-lock.json* ./ RUN npm install --legacy-peer-deps # Rebuild the source code only when needed FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules # Copy the entire project COPY . . # Generate Prisma client RUN npx prisma generate # Next.js collects completely anonymous telemetry data about general usage. ENV NEXT_TELEMETRY_DISABLED 1 ENV DOCKER_BUILD 1 ENV CI_BUILD_MODE 1 # Build the app RUN npm run build # Production image, copy all the files and run next FROM base AS runner WORKDIR /app ENV NODE_ENV production ENV NEXT_TELEMETRY_DISABLED 1 # Install all dependencies to satisfy entrypoint requirements COPY package.json package-lock.json* ./ RUN npm install --legacy-peer-deps # Install Prisma client with exact version match RUN npm uninstall prisma @prisma/client --legacy-peer-deps RUN npm install prisma@6.3.1 @prisma/client@6.3.1 --legacy-peer-deps # Install tsx explicitly RUN npm install -g tsx # Install esbuild for widget RUN npm install esbuild --legacy-peer-deps # Install JSDOC RUN npm install -g jsdoc # Add bash and curl for the entry script and health checks RUN apk add --no-cache bash curl # Copy the entire project from the builder stage COPY --from=builder /app . # Copy maintenance page and server script COPY scripts/maintenance/index.html ./index.html COPY scripts/maintenance/server.js ./scripts/maintenance/server.js # Copy entrypoint script and fix line endings COPY docker-entrypoint-compose.sh /app/docker-entrypoint-compose.sh RUN sed -i 's/\r$//' /app/docker-entrypoint-compose.sh && chmod +x /app/docker-entrypoint-compose.sh EXPOSE 3000 ENV PORT 3000 ENV HOSTNAME "0.0.0.0" # Use npm script that explicitly calls bash ENTRYPOINT ["bash", "docker-entrypoint-compose.sh"] ================================================ FILE: LICENSE ================================================ CHANGERAWR NON-COMMERCIAL OPEN SOURCE LICENSE Copyright (c) 2025 Supernova Software, LLC. All rights reserved. This software and associated documentation files (the "Software") are made available under the terms of this license. The source code is publicly accessible and may be freely used, modified, and distributed subject to the restrictions outlined below. GRANT OF LICENSE Supernova Software, LLC grants you a worldwide, royalty-free, non-exclusive license to use, copy, modify, merge, publish, and distribute the Software and derivative works thereof, subject to the following conditions. PERMITTED USES You may freely: 1. Use the Software for any purpose, including commercial purposes, provided you do not profit from the Software itself. 2. Modify the Software and create derivative works, including closed-source versions, provided you comply with the terms of this license. 3. Fork the repository and maintain your own version of the Software. 4. Distribute the Software, modified or unmodified, in source or binary form. 5. Deploy the Software on your own infrastructure, including for use in commercial organizations and business operations. 6. Contribute improvements, bug fixes, and features back to the project. PROHIBITED USES The following uses are strictly prohibited unless you are Supernova Software, LLC or an entity explicitly authorized in writing by Supernova Software, LLC: 1. Profiting from the Software: You may not sell, rent, lease, license, or otherwise monetize the Software or any derivative work. This includes but is not limited to: - Selling access to the Software as a product or service - Charging fees for hosted instances of the Software - Offering the Software as part of a paid subscription or service - Selling modified or unmodified versions of the Software - Generating revenue directly from providing access to the Software's functionality 2. Billing System Integration: You may not integrate any billing system, payment processor, paywall, subscription mechanism, or any other monetization feature into the Software or derivative works that would allow charging users for the Software itself or its features. This prohibition applies regardless of whether the fork is open source or closed source. 3. Trademark Misuse: You may not use the names "Changerawr," "Supernova3339," or "Supernova Software, LLC," along with any associated logos or branding, in a manner that suggests endorsement or affiliation without express written permission. 4. Copyright Removal: You may not remove, obscure, or alter any copyright notices, license terms, or attribution present in the Software or documentation under any circumstances. This applies even if you use only a portion of the code. CLARIFICATION OF COMMERCIAL USE You MAY use the Software in a commercial environment (e.g., within your business, as a tool for your operations, or as part of your internal infrastructure). What you CANNOT do is make money by selling the Software itself or charging others for access to it. CONTRIBUTIONS By submitting code, documentation, or other materials to this project: 1. You grant Supernova Software, LLC an irrevocable, perpetual, worldwide, royalty-free license to use, reproduce, modify, display, distribute, and sublicense your contributions in any manner, including in commercial products. 2. You represent that you have all necessary rights to grant this license and that your contributions do not infringe any third party rights. 3. You acknowledge that Supernova Software, LLC has no obligation to incorporate your contributions and may use them without restriction. INTELLECTUAL PROPERTY AND ATTRIBUTION All copyright and intellectual property rights in the original Software remain with Supernova Software, LLC. While the Software is open source and freely available, this license does not transfer ownership of the intellectual property. All copies and derivative works must retain the original copyright notice and this license. Modified versions should clearly indicate what changes were made and by whom. CRITICAL: Even if you extract or use only a portion of the code from this Software, you MUST retain all original copyright notices in your project. This requirement applies to all uses without exception, including closed-source forks, commercial applications, and packages published on any registry. ENFORCEMENT Violation of this license constitutes copyright infringement and breach of contract. Supernova Software, LLC reserves the right to pursue all available legal remedies, including injunctive relief and monetary damages, against any party that violates these terms. TERMINATION This license is effective until terminated. Supernova Software, LLC may terminate your rights under this license immediately if you fail to comply with any term. Upon termination: 1. You must immediately cease all use of the Software. 2. You must destroy all copies of the Software in your possession or control. 3. You must certify in writing to Supernova Software, LLC that you have complied with these requirements, if requested. Termination does not limit any other rights or remedies available to Supernova Software, LLC. FEATURE GUARANTEE Supernova Software, LLC guarantees that all features of Changerawr will remain free and accessible at no cost, regardless of future developments or service offerings. This guarantee applies to the core Software as distributed in the public repository. The sole exception to this guarantee is if Supernova Software, LLC creates an official managed cloud hosting service. In such case, fees may be charged exclusively to cover: - Server and infrastructure costs for the client's virtual private server (VPS) - Management and maintenance services - Service-based add-ons that do not affect the core Software functionality Service-based add-ons are limited to operational services such as prioritized support, dedicated single sign-on (SSO) infrastructure, enhanced SLA agreements, or professional services. Add-ons will NEVER include software features, integrations, or functionality that affects the Changerawr core itself. All software features and integrations remain free in both self-hosted and any potential cloud offerings. Any such cloud offering would be optional, and the self-hosted version of Changerawr would remain completely free with all features intact. WARRANTY DISCLAIMER THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE. THIS INCLUDES, WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, OR NON-INFRINGEMENT. SUPERNOVA SOFTWARE, LLC MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY, RELIABILITY, AVAILABILITY, TIMELINESS, SECURITY, OR ACCURACY OF THE SOFTWARE. NO WARRANTIES ARE PROVIDED UNLESS EXPLICITLY STATED IN A SEPARATE WRITTEN AGREEMENT BETWEEN YOU AND SUPERNOVA SOFTWARE, LLC. THE ABSENCE OF SUCH AN AGREEMENT MEANS YOU RECEIVE THE SOFTWARE WITH ABSOLUTELY NO WARRANTY WHATSOEVER. LIMITATION OF LIABILITY IN NO EVENT SHALL SUPERNOVA SOFTWARE, LLC BE LIABLE FOR ANY SPECIAL, INCIDENTAL, INDIRECT, OR CONSEQUENTIAL DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE THE SOFTWARE, EVEN IF SUPERNOVA SOFTWARE, LLC HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. GOVERNING LAW This license shall be governed by and construed in accordance with the laws of the jurisdiction in which Supernova Software, LLC is registered, without regard to its conflict of law provisions. Any legal action or proceeding arising under this license shall be brought exclusively in the courts of that jurisdiction. SEVERABILITY If any provision of this license is found to be unenforceable or invalid, that provision shall be limited or eliminated to the minimum extent necessary so that this license shall otherwise remain in full force and effect. ENTIRE AGREEMENT This license constitutes the entire agreement between you and Supernova Software, LLC concerning the Software and supersedes any prior agreements or understandings. CONTACT For licensing inquiries, alternative licensing arrangements, or clarification of these terms, contact Supernova Software, LLC through https://github.com/Supernova3339/changerawr. NO WAIVER The failure of Supernova Software, LLC to enforce any right or provision of this license shall not constitute a waiver of such right or provision. ================================================ FILE: PROJECT_INDEX.md ================================================ # Project Index: Changerawr Generated: 2026-04-15 ## Overview Changerawr is a self-hosted changelog management platform with AI assistance, custom domains, SSO/SAML, Slack/GitHub integrations, and embeddable widgets. **License**: CNC OSL (Non-Commercial Open Source) --- ## 📁 Project Structure ``` changerawr/ ├── app/ # Next.js App Router (pages + API routes) │ ├── (auth)/ # Auth page group (login, register, setup, 2FA) │ ├── (email)/ # Email-related pages (unsubscribed) │ ├── api/ # 145+ API route handlers │ ├── dashboard/ # Authenticated dashboard │ │ ├── admin/ # Admin panel (users, system config, SSO, audit) │ │ ├── projects/ # Project management & changelog editor │ │ └── settings/ # User settings │ ├── changelog/ # Public changelog pages + RSS feeds │ └── .well-known/ # ACME challenge routes ├── components/ # React UI components by feature ├── lib/ # Business logic, utils, auth, services ├── hooks/ # 13 custom React hooks ├── prisma/schema/ # Modular Prisma schema (6 files) ├── emails/ # Email template components ├── context/ # React context providers (auth, setup) ├── widgets/ # Embeddable changelog widget source ├── scripts/ # Build & maintenance scripts └── public/ # Static assets ``` --- ## 🚀 Entry Points | Path | Purpose | |------|---------| | `app/layout.tsx` | Root layout with providers | | `app/page.tsx` | Root redirect | | `app/(auth)/setup/page.tsx` | First-run setup wizard | | `lib/api/middleware.ts` | API request middleware (auth, permissions) | | `next.config.ts` | Next.js config (React Compiler, Turbopack) | | `docker-entrypoint.sh` | Container startup | --- ## 🔐 Authentication System - **Token**: JWT in `accessToken` cookie — verified via `verifyAccessToken` from `lib/auth/tokens` - **Methods**: Password, OAuth, SAML/SSO, Passkey/WebAuthn - **Roles**: `ADMIN`, `STAFF`, `VIEWER` - **2FA Modes**: `NONE`, `PASSKEY_PLUS_PASSWORD`, `PASSWORD_PLUS_PASSKEY` - **CLI Auth**: Token-based code flow at `/api/auth/cli/*` Key auth files: - `lib/auth/tokens.ts` — JWT generation/verification - `lib/auth/oauth.ts` — OAuth 2.0 provider integration - `lib/auth/saml.ts` — SAML implementation - `lib/auth/webauthn.ts` — Passkey/WebAuthn - `lib/api/permissions.ts` — Permission checking - `lib/api/route-permissions.ts` — Route-level permission config --- ## 📦 Core Modules ### API Layer (`app/api/`) Route groups: - `auth/` — Login, register, OAuth, SAML, passkeys, CLI, password reset - `admin/` — Config, users, AI settings, SSO providers, API keys, audit logs - `projects/[projectId]/` — CRUD, changelog entries, integrations, analytics - `changelog/` — Public access, subscriptions, RSS - `custom-domains/` — Domain verify, SSL management, browser rules - `acme/` — Let's Encrypt certificate issuance/renewal - `integrations/slack/` — Slack OAuth callback - `config/timezone` — Public effective timezone (no auth) - `health` — Health check ### Services Layer (`lib/services/`) Business logic separated from API handlers: - `analytics/` — Analytics data processing - `changelog/` — Entry CRUD, publishing, scheduling - `email/` — SMTP sending, newsletter management - `github/` — Commit sync, tag creation - `slack/` — Slack bot notifications - `jobs/` — Background job execution (ScheduledJob queue) - `projects/` — Project operations - `sponsor/` — License/sponsor management - `telemetry/` — Telemetry tracking - `search/` — Full-text PostgreSQL search - `core/markdown/` — Markdown parsing & custom extensions ### Auth (`lib/auth/`) See Authentication System above. ### Utils (`lib/utils/`) - `format-date.ts` — Timezone-aware date formatting - `cookies.ts` — Cookie helpers - `encryption.ts` — Encryption utilities - `auditLog.ts` — Audit log helpers - `api.ts` — API utilities ### Custom Domains (`lib/custom-domains/`) - `service.ts` — Domain management - `ssl/` — ACME/Let's Encrypt logic - `dns.ts` — DNS verification utilities --- ## 🗄️ Database (Prisma + PostgreSQL) Schema split across `prisma/schema/`: - `base.prisma` — Datasource & generator - `users.prisma` — User, OAuth, SAML, Passkey, 2FA, Invite, PasswordReset - `projects.prisma` — Project, Changelog, ChangelogEntry, ChangelogTag, Widget - `system.prisma` — SystemConfig, ApiKey, AuditLog, ScheduledJob, Analytics, CustomDomain - `integrations.prisma` — EmailConfig, SlackIntegration, GitHubIntegration, Subscribers - `enums.prisma` — All enum definitions Key models: - `User` — Core user with role, timezone - `SystemConfig` — Global app config (timezone, email, AI, Slack OAuth, customDateTemplates as JSONB) - `Project` / `Changelog` / `ChangelogEntry` — Main content models - `ScheduledJob` — Background job queue (publish, email, SSL renewal, telemetry) - `CustomDomain` / `DomainCertificate` — Domain management with SSL --- ## 🧩 Components ``` components/ ├── changelog/editor/ # Entry editor (AI, versioning, scheduling) │ └── VersionSelector.tsx # Version/date template picker ├── markdown-editor/ # Custom markdown editor with AI │ ├── MarkdownEditor.tsx │ ├── MarkdownToolbar.tsx │ ├── MarkdownPreview.tsx │ └── ai/ # AI assistant panel ├── admin/ # Admin UI (API keys, audit logs, requests) ├── analytics/ # Chart components ├── project/ # Project sidebar, navigation, settings │ └── catch-up/ # Feature recap display ├── sso/ # SSO configuration UI ├── setup/ # First-run setup wizard ├── settings/ # User security settings ├── ui/ # Shadcn/Radix UI primitives ├── CommandPalette.tsx # Global command palette └── Logo.tsx ``` --- ## 🪝 Hooks (`hooks/`) | Hook | Purpose | |------|---------| | `use-timezone.ts` | Resolves effective timezone (user → system → UTC) | | `useAIAssistant.ts` | AI writing assistant | | `useMarkdownState.ts` | Markdown editor state management | | `useEditorHistory.ts` | Undo/redo for editor | | `useSlashCommands.ts` | Slash command handling | | `useCommandPalette.ts` | Command palette state | | `useBookmarks.ts` | Bookmark management | | `useChunkedData.ts` | Data chunking for large lists | | `useTelemetry.ts` | Telemetry tracking | | `useWhatsNew.ts` | What's new modal | --- ## ⚙️ Configuration | File | Purpose | |------|---------| | `next.config.ts` | Next.js (React Compiler on, strictMode off, Turbopack) | | `tailwind.config.ts` | Tailwind CSS | | `components.json` | shadcn/ui component config | | `tsconfig.json` | TypeScript (strict, `@/*` alias) | | `prisma/schema/` | Database schema | | `.env.example` | Required environment variables | | `Dockerfile` + `docker-compose.yml` | Container deployment | | `Caddyfile` / `nginx.conf` | Reverse proxy configs | --- ## 📚 Documentation | File | Topic | |------|-------| | `README.md` | Features, quick start, deployment | | `CHANGELOG.md` | Version history | | `APIDOCGUIDE.md` | API documentation guide | | `ideas.md` | Feature ideas/roadmap | | `issues.md` | Known issues | | `useful-information-for-development/` | Dev notes (Slack scopes, etc.) | --- ## 🔗 Key Dependencies | Package | Version | Purpose | |---------|---------|---------| | next | 16.1.6 | Framework | | react | 19.2.4 | UI library | | prisma | 6.7.0 | ORM | | jose | — | JWT tokens | | @node-saml/node-saml | — | SAML/SSO | | @simplewebauthn/* | — | Passkeys | | @tiptap/react | — | Rich text editing | | @tanstack/react-query | — | Server state | | @scalar/nextjs-api-reference | — | API docs UI | | recharts | — | Analytics charts | | framer-motion | — | Animations | | zod | — | Schema validation | | react-hook-form | — | Form handling | | nodemailer | — | Email sending | | @slack/bolt | — | Slack integration | --- ## 📝 Quick Start 1. `cp .env.example .env` and fill in required vars 2. `npm install` 3. `npx prisma migrate dev` — run DB migrations 4. `npx prisma generate` — generate Prisma client 5. `npm run dev` — starts on port 3001 6. Visit `/setup` on first run **Build widget**: `npm run build:widget` **API docs**: `npm run generate-swagger` → visit `/api-docs` --- ## 🏗️ Architectural Patterns 1. **Auth**: JWT in `accessToken` cookie; `verifyAccessToken` from `lib/auth/tokens` 2. **Permissions**: Role-based, configured in `lib/api/route-permissions.ts` 3. **Services**: Business logic in `lib/services/`, not in route handlers 4. **Timezone**: User.timezone → SystemConfig.timezone → UTC; use `useTimezone()` hook client-side 5. **Admin layout**: `/^\/dashboard\/admin\/system/` pattern catches sub-pages 6. **SystemConfig**: Single row, full object sent on PATCH — new fields need defaults 7. **Background jobs**: `ScheduledJob` model polled by cron endpoints 8. **Custom domains**: DNS verification + Let's Encrypt via ACME protocol ================================================ FILE: README.md ================================================

logo
Ship, Change, Rawr

[![Version](https://img.shields.io/badge/version-1.0.5-blue.svg)](https://github.com/supernova3339/changerawr) [![Status](https://img.shields.io/badge/status-Production%20Ready-green.svg)](https://github.com/supernova3339/changerawr) [![License](https://img.shields.io/badge/license-CNC%20OSL-purple.svg)](LICENSE) # What is Changerawr? Changerawr lets you write down what you changed, then share those changes with people. You write entries about updates you made, and Changerawr gives you ways to display them - like widgets for your website, public pages people can visit, or APIs to use however you want. \ You can think of it as a **Changelog Management System** [CMS] If you don't know what a changelog is, check out [betterauth](https://www.better-auth.com/changelogs) for an example! ## ✨ Why Changerawr? **Developer-focused.** Headless API, beautiful documentation, SDKs, integrations, and a CLI. **Fully customizable.** Do things your way. No vendor lock-in, no forced workflows. **For everyone.** Whether you're a solo developer, small business, or enterprise team - Changerawr scales with you. ( yes, this means you can use it for commercial usage! just please do reach out if you do, I would love to know how your using Changerawr! ) ## 🚀 Features - **📝 Beautiful Content Editor** - Write changelogs that look professional - **🤖 AI-Powered** - Let AI help you write better changelog entries - **📡 Headless API** - Beautifully documented REST API for full control - **🧩 SDKs** - Pre-built libraries for popular languages - **🎨 Embeddable Widget** - Drop a changelog widget anywhere on your site - **📧 Email Notifications** - Keep users informed of updates - **🏷️ Tags & Versioning** - Organize entries exactly how you want - **🔗 Multiple Integrations** - Connect with your existing tools - **🔐 Modern Authentication** - Custom-built auth with passkey support - **🖥️ Desktop-First Design** - Built for desktop use (mobile works, but it's quirky) - **🔍 Full-Text Search** - Search everything, instantly - -**🌐 Custom Domains** - Link a custom domain to your changelog ## 🚀 Quick Start ### Prerequisites - Node.js 22+ - PostgreSQL database ### Installation ```bash # Clone the repository git clone https://github.com/supernova3339/changerawr.git cd changerawr # Install dependencies npm install # Set up environment cp .env.example .env.local # Edit .env.local with your settings # Set up database npx prisma generate npx prisma migrate deploy # Build the widget npm run build:widget # Start development server npm run dev ``` Visit [http://localhost:3000](http://localhost:3000) and you're ready to go! ### Docker Setup ```bash docker-compose up --build ``` ## ⚙️ Configuration ### Environment Variables ```bash # Database DATABASE_URL="postgresql://postgres@localhost:5432/changerawr?schema=public" # Authentication JWT_ACCESS_SECRET="your-jwt-secret-key" NEXT_PUBLIC_APP_URL="http://localhost:3000" # GitHub Integration (optional) GITHUB_ENCRYPTION_KEY="your-github-encryption-key" # Analytics ANALYTICS_SALT="your-secure-random-salt-here" ``` ## 📦 Widget Integration The easiest way to add changelogs to your site - perfect for non-technical users: ```html ``` ### Widget Options | Option | Type | Default | Description | |--------|---------|:---------------:|-------------| | `data-theme` | string | "light" | Theme: "light" or "dark" | | `data-position` | string | "bottom-right" | Popup position | | `data-max-height` | string | "400px" | Maximum height | | `data-popup` | boolean | false | Enable popup mode | | `data-trigger` | string | null | Button ID or "immediate" | | `data-max-entries` | number | 3 | Amount of entries to display, min 3 max 10 ## 🛠️ Tech Stack **Built with modern, reliable technologies:** - **Next.js 16** - React framework with App Router - **Prisma ORM** - Type-safe database access - **PostgreSQL** - Robust, scalable database - **Shadcn/UI** - Beautiful, accessible UI components - **TypeScript** - Full type safety throughout ## 🏗️ Development ### Available Scripts ```bash npm run dev # Development server npm run build # Production build npm run start # Start built development serer npm run start:prod # Start production server npm run start:prod:win # Start production server ( Windows ) npm run build:widget # Build embeddable widget npm run generate-swagger # Generate API docs npm run lint # Code linting ( next 16 will depc this - note ) npm run maintenance # Run the maintenance page npm run start:with-maintenance # Runs maintenance page and the main server npm run prisma:studio # Database viewer and manager ``` ### Project Structure ``` changerawr/ ├── app/ # Next.js App Router │ ├── (auth)/ # Auth pages │ ├── (email)/ # Newsletter related pages │ ├── api/ # API endpoints │ ├── api-docs/ # API Documentation | ├── changelog/ # Changelog pages (public/custom-domain) │ ├── cli/ # Internal pages used to interface with the Changerawr CLI │ └── dashboard/ # Main app ├── components/ # React components ├── lib/ # Core utilities ├── prisma/ # Database schema ├── widgets/ # Widget source ├── scripts/ # Build scripts └── emails/ # Email templates ``` ## 🚢 Deployment ### Docker (Recommended) ```bash # Build docker build -t changerawr . # Run docker run -p 3000:3000 \ -e DATABASE_URL="your-database-url" \ -e JWT_ACCESS_SECRET="your-secret" \ -e NEXT_PUBLIC_APP_URL="your-app-url" \ -e GITHUB_ENCRYPTION_KEY="your-encryption-key-32-chars" \ -e ANALYTICS_SALT="your-analytics-salt" \ changerawr ``` ### Manual Deployment ```bash npm run build npx prisma migrate deploy npm run build:widget npm run generate-swagger npm start:with-maintenance ``` ## 🎯 Features in Detail ### AI-Powered Writing Let AI help you craft professional changelog entries that your users will actually want to read. ### Custom Authentication Built from scratch with modern features like passkeys. No third-party restrictions, full control. ### Developer-First API Clean, well-documented REST API with SDKs for popular languages. Build exactly what you need. ### Email Notifications Keep your users in the loop with beautiful email updates when you ship new features. ### Full Customization Tags, versioning - organize your changelogs exactly how your team works. ## 🤝 Contributing We welcome contributions! Whether it's: - 🐛 Bug fixes - ✨ New features - 📖 Documentation improvements - 🎨 UI/UX enhancements 1. Fork the repo 2. Create a feature branch 3. Make your changes 4. Submit a pull request ## 📄 License Non-Commercial Open Source License - see [LICENSE](LICENSE) for details. Changerawr is open source and free to use, modify, and fork (including closed-source versions). You can use it commercially in your business, but you cannot profit from selling the software itself or charge users for access to it. All features remain free forever. ## 🙋‍♂️ Support - 🐛 **Issues**: [GitHub Issues](https://github.com/supernova3339/changerawr/issues) - 💬 **Discussions**: [GitHub Discussions](https://github.com/supernova3339/changerawr/discussions) --- **Built by developers, for developers.** ================================================ FILE: app/(auth)/forgot-password/layout.tsx ================================================ import React from 'react'; import { Metadata } from 'next'; export const metadata: Metadata = { title: 'Forgot Password - Changerawr', description: 'Reset your password for Changerawr', }; export default function ForgotPasswordLayout({ children, }: { children: React.ReactNode; }) { return (

Changerawr

{children}

© {new Date().getFullYear()} Changerawr. All rights reserved.

); } ================================================ FILE: app/(auth)/forgot-password/page.tsx ================================================ 'use client'; import React, { useState, useEffect, useRef } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import Link from 'next/link'; import { Card, CardContent, CardFooter, } from "@/components/ui/card"; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { toast } from '@/hooks/use-toast'; import { ArrowLeft, Loader2, CheckCircle2, AlarmClock, Mail, RefreshCw, Copy, HelpCircle, MailCheck, ExternalLink } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import confetti from 'canvas-confetti'; const forgotPasswordSchema = z.object({ email: z.string().email('Please enter a valid email address'), }); type ForgotPasswordFormValues = z.infer; // Email providers and their URLs const emailProviders = { 'gmail.com': 'https://mail.google.com', 'outlook.com': 'https://outlook.live.com', 'hotmail.com': 'https://outlook.live.com', 'yahoo.com': 'https://mail.yahoo.com', 'icloud.com': 'https://www.icloud.com/mail', 'protonmail.com': 'https://mail.proton.me', 'naver.com': 'https://mail.naver.com/' }; // Smart confetti function const fireConfetti = (type: 'success' | 'resend' = 'success') => { const isMobile = window.innerWidth < 768; const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0, disableForReducedMotion: true }; // Check if reduced motion is preferred const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReducedMotion) { // Only show minimal confetti for users who prefer reduced motion confetti({ ...defaults, particleCount: 20, gravity: 1, origin: { y: 0.6, x: 0.5 } }); return; } if (type === 'success') { // Initial burst from the center confetti({ ...defaults, particleCount: isMobile ? 50 : 100, origin: { y: 0.6, x: 0.5 } }); // Create cannon effect setTimeout(() => { confetti({ ...defaults, particleCount: isMobile ? 25 : 50, angle: 60, spread: 50, origin: { x: 0, y: 0.6 } }); confetti({ ...defaults, particleCount: isMobile ? 25 : 50, angle: 120, spread: 50, origin: { x: 1, y: 0.6 } }); }, 250); // Final smaller bursts setTimeout(() => { confetti({ ...defaults, particleCount: isMobile ? 15 : 30, angle: 90, gravity: 1.2, origin: { x: 0.5, y: 0.7 } }); }, 400); } else { // Simpler confetti for resend confetti({ ...defaults, particleCount: isMobile ? 30 : 50, origin: { y: 0.6, x: 0.5 }, gravity: 1.2 }); } }; export default function ForgotPasswordPage() { const [isSubmitting, setIsSubmitting] = useState(false); const [isEmailSent, setIsEmailSent] = useState(false); const [sentEmail, setSentEmail] = useState(''); const [error, setError] = useState(''); const [countdown, setCountdown] = useState(0); const [emailProvider, setEmailProvider] = useState(null); const [lastTyped, setLastTyped] = useState(0); const [isCopied, setIsCopied] = useState(false); const wrapperRef = useRef(null); const { register, handleSubmit, watch, formState: { errors, isValid }, reset, } = useForm({ resolver: zodResolver(forgotPasswordSchema), mode: 'onChange', }); const email = watch('email', ''); // Handle countdown for resend functionality useEffect(() => { if (countdown > 0) { const timer = setTimeout(() => setCountdown(countdown - 1), 1000); return () => clearTimeout(timer); } }, [countdown]); // Detect email provider for quick link useEffect(() => { if (sentEmail) { const domain = sentEmail.split('@')[1]?.toLowerCase(); const provider = domain && Object.keys(emailProviders).find(key => domain === key || domain?.endsWith(`.${key}`) ); setEmailProvider(provider || null); } }, [sentEmail]); // Handle email typing suggestions useEffect(() => { if (email && !isSubmitting && !isEmailSent) { setLastTyped(Date.now()); } }, [email, isSubmitting, isEmailSent]); // Scroll to top when success view is shown useEffect(() => { if (isEmailSent && wrapperRef.current) { wrapperRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [isEmailSent]); const onSubmit = async (data: ForgotPasswordFormValues) => { setError(''); setIsSubmitting(true); try { const response = await fetch('/api/auth/forgot-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to request password reset'); } // Set 60-second countdown for resend button setCountdown(60); setSentEmail(data.email); setIsEmailSent(true); // Smart confetti - delayed to match animation setTimeout(() => fireConfetti('success'), 200); toast({ title: 'Reset Email Sent', description: 'Check your inbox for the password reset link', variant: 'success', }); } catch (error) { setError(error instanceof Error ? error.message : 'Something went wrong'); toast({ title: 'Error', description: error instanceof Error ? error.message : 'Something went wrong', variant: 'destructive', }); } finally { setIsSubmitting(false); } }; const handleResend = () => { // Simpler confetti for resend setTimeout(() => fireConfetti('resend'), 100); onSubmit({ email: sentEmail }); }; const handleTryDifferentEmail = () => { setIsEmailSent(false); setError(''); reset(); setTimeout(() => { document.getElementById('email')?.focus(); }, 300); }; const copyToClipboard = () => { if (sentEmail) { navigator.clipboard.writeText(sentEmail); setIsCopied(true); toast({ title: 'Copied', description: 'Email address copied to clipboard', }); // Reset the copied state after 2 seconds setTimeout(() => setIsCopied(false), 2000); } }; const getEmailDomainSuggestion = () => { if (!email || email.includes('@') || Date.now() - lastTyped < 1000) return null; const commonDomains = ['gmail.com', 'outlook.com', 'yahoo.com', 'icloud.com']; return commonDomains[0]; // Suggest the first common domain }; const suggestion = getEmailDomainSuggestion(); return (
{isEmailSent ? (

Check your inbox

We've sent a password reset link to:

{sentEmail}

Copy email address

{emailProvider && ( )}

Didn't receive the email? Check your spam folder or try another email address.

) : (

Forgot Password

Enter your email address and we'll send you a link to reset your password

{error && ( {error} )}

Enter the email address you used to register

{suggestion && (
)}
{errors.email && ( ⚠️ {errors.email.message} )}
)}
); } ================================================ FILE: app/(auth)/layout.tsx ================================================ 'use client' import { useAuth } from '@/context/auth' import { useRouter, usePathname } from 'next/navigation' import React, { useEffect } from 'react' export default function AuthLayout({ children, }: { children: React.ReactNode }) { const { user, isLoading } = useAuth() const router = useRouter() const pathname = usePathname() useEffect(() => { if (!isLoading && user) { router.replace('/dashboard') } }, [user, isLoading, router, pathname]) if (isLoading) { return (
Loading...
) } return (
{children}
) } ================================================ FILE: app/(auth)/login/layout.tsx ================================================ import React from 'react'; import { Metadata } from 'next'; export const metadata: Metadata = { title: 'Login - Changerawr', description: 'Login to your Changerawr account', }; export default function RegisterLayout({ children, }: { children: React.ReactNode; }) { return (

Changerawr

{children}

© {new Date().getFullYear()} Changerawr. All rights reserved.

); } ================================================ FILE: app/(auth)/login/page.tsx ================================================ 'use client' import React, {useEffect, useState} from 'react' import {useForm} from 'react-hook-form' import {zodResolver} from '@hookform/resolvers/zod' import {z} from 'zod' import {useAuth} from '@/context/auth' import Link from "next/link" import {useQuery} from '@tanstack/react-query' import {motion, AnimatePresence} from 'framer-motion' import confetti from 'canvas-confetti' import { startAuthentication, browserSupportsWebAuthn, } from '@simplewebauthn/browser' // UI Components import {Input} from '@/components/ui/input' import {Button} from '@/components/ui/button' import {Label} from '@/components/ui/label' import {Alert, AlertTitle, AlertDescription} from '@/components/ui/alert' import {Avatar, AvatarImage, AvatarFallback} from "@/components/ui/avatar" import {Card, CardContent, CardFooter} from '@/components/ui/card' import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip" // Icons import { ArrowLeft, User, Fingerprint, Eye, EyeOff, Loader2, Lock, Mail, CheckCircle2, AlertTriangle, Shield, RefreshCw, ArrowRight, Key } from 'lucide-react' import {ProviderLogo} from "@/components/sso/ProviderLogo"; const emailSchema = z.object({ email: z.string().email('Please enter a valid email') }) const passwordSchema = z.object({ password: z.string().min(1, 'Please enter your password') }) type EmailForm = z.infer type PasswordForm = z.infer interface UserPreview { name: string | null email: string avatarUrl: string } interface OAuthProvider { id: string name: string enabled: boolean isDefault: boolean } interface PasswordBreachData { breachCount: number resetUrl: string } // Smart confetti function from registration page const fireConfetti = () => { const isMobile = window.innerWidth < 768; const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0, disableForReducedMotion: true }; // Check if reduced motion is preferred const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReducedMotion) { // Only show minimal confetti for users who prefer reduced motion confetti({ ...defaults, particleCount: 20, gravity: 1, origin: {y: 0.6, x: 0.5} }); return; } // Initial burst from the center confetti({ ...defaults, particleCount: isMobile ? 50 : 100, origin: {y: 0.6, x: 0.5} }); // Create cannon effect setTimeout(() => { confetti({ ...defaults, particleCount: isMobile ? 25 : 50, angle: 60, spread: 50, origin: {x: 0, y: 0.6} }); confetti({ ...defaults, particleCount: isMobile ? 25 : 50, angle: 120, spread: 50, origin: {x: 1, y: 0.6} }); }, 250); // Final smaller bursts setTimeout(() => { confetti({ ...defaults, particleCount: isMobile ? 15 : 30, angle: 90, gravity: 1.2, origin: {x: 0.5, y: 0.7} }); }, 400); }; export default function LoginPage() { const {user, isLoading: authLoading} = useAuth() const [error, setError] = useState('') const [step, setStep] = useState<'email' | 'password' | 'breach-warning'>('email') const [userPreview, setUserPreview] = useState(null) const [passwordBreach, setPasswordBreach] = useState(null) const [supportsWebAuthn, setSupportsWebAuthn] = useState(false) const [isAuthenticating, setIsAuthenticating] = useState(false) const [showPassword, setShowPassword] = useState(false) const [isSuccess, setIsSuccess] = useState(false) const [redirectTo, setRedirectTo] = useState('/dashboard') // Fetch OAuth providers const {data: oauthProviders, isLoading: isLoadingProviders} = useQuery({ queryKey: ['oauthProviders'], queryFn: async () => { try { const response = await fetch('/api/auth/oauth/providers') if (!response.ok) return [] const data = await response.json() return data.providers } catch (error) { console.error('Failed to fetch OAuth providers:', error) return [] } }, staleTime: 60000 // 1 minute }) // Fetch SAML providers const {data: samlProviders, isLoading: isLoadingSAMLProviders} = useQuery({ queryKey: ['samlProviders'], queryFn: async () => { try { const response = await fetch('/api/auth/saml/providers') if (!response.ok) return [] const data = await response.json() return data.providers } catch (error) { console.error('Failed to fetch SAML providers:', error) return [] } }, staleTime: 60000 }) const emailForm = useForm({ resolver: zodResolver(emailSchema), defaultValues: { email: '' } }) const passwordForm = useForm({ resolver: zodResolver(passwordSchema), defaultValues: { password: '' } }) useEffect(() => { setSupportsWebAuthn(browserSupportsWebAuthn()) }, []) useEffect(() => { const searchParams = new URLSearchParams(window.location.search) const redirectParam = searchParams.get('redirectTo') || searchParams.get('from') if (redirectParam) { setRedirectTo(redirectParam) } const handleOAuthRedirect = async () => { const oauthComplete = searchParams.get('oauth_complete') if (oauthComplete === 'true') { try { // Show success state and confetti, then redirect setIsSuccess(true) setTimeout(() => { fireConfetti() }, 300) // Redirect with window.location after a short delay setTimeout(() => { window.location.href = redirectTo || '/dashboard' }, 1500) } catch (err) { console.error('OAuth redirect error:', err) setError('Failed to complete login') } } } if (user && !authLoading) { // Show success state and confetti, then redirect setIsSuccess(true) setTimeout(() => { fireConfetti() }, 300) setTimeout(() => { window.location.href = redirectTo }, 1500) } else { // Check for OAuth redirects only if not already logged in handleOAuthRedirect() } // Check for error in URL (typically from OAuth callback) const errorParam = searchParams.get('error') if (errorParam) { setError(decodeURIComponent(errorParam)) } }, [user, authLoading, redirectTo]) const formatBreachCount = (count: number): string => { if (count >= 1000000) { return `${(count / 1000000).toFixed(1)}M`; } else if (count >= 1000) { return `${(count / 1000).toFixed(1)}K`; } return count.toLocaleString(); }; const onEmailSubmit = async (data: EmailForm) => { try { setError('') const response = await fetch('/api/auth/preview', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({email: data.email.toLowerCase()}) }) if (!response.ok) { throw new Error('Authentication failed') } const userData = await response.json() setUserPreview(userData) setStep('password') } catch (err: unknown) { setError('Unable to find your account') console.log(err) } } const onPasswordSubmit = async (data: PasswordForm) => { try { setError('') if (!userPreview) return const response = await fetch('/api/auth/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ email: userPreview.email, password: data.password, bypassBreachWarning: false // Initial attempt without bypass }), credentials: 'include' }) const responseData = await response.json() // Handle password breach warning if (response.status === 422 && responseData.error === 'password_breached') { setPasswordBreach({ breachCount: responseData.breachCount, resetUrl: responseData.resetUrl }); setStep('breach-warning') return; } // Handle 2FA requirement if (response.status === 403 && responseData.requiresSecondFactor) { sessionStorage.setItem('2faSessionToken', responseData.sessionToken) sessionStorage.setItem('2faType', responseData.secondFactorType) window.location.href = '/two-factor' return } if (!response.ok) { throw new Error(responseData.error || 'Authentication failed') } // Success state with confetti setIsSuccess(true) setTimeout(() => { fireConfetti() }, 300) // Redirect with window.location setTimeout(() => { window.location.href = redirectTo }, 1500) } catch (error) { setError(error instanceof Error ? error.message : 'Authentication failed') passwordForm.reset() } } // Handle continuing despite breach warning const handleContinueWithBreachedPassword = async () => { try { setError('') if (!userPreview) return const response = await fetch('/api/auth/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ email: userPreview.email, password: passwordForm.getValues('password'), bypassBreachWarning: true // Bypass the breach warning }), credentials: 'include' }) const responseData = await response.json() // Handle 2FA requirement if (response.status === 403 && responseData.requiresSecondFactor) { sessionStorage.setItem('2faSessionToken', responseData.sessionToken) sessionStorage.setItem('2faType', responseData.secondFactorType) window.location.href = '/two-factor' return } if (!response.ok) { throw new Error(responseData.error || 'Authentication failed') } // Success state with confetti setIsSuccess(true) setTimeout(() => { fireConfetti() }, 300) // Redirect with window.location setTimeout(() => { window.location.href = redirectTo }, 1500) } catch (error) { setError(error instanceof Error ? error.message : 'Authentication failed') setStep('password') // Go back to password form setPasswordBreach(null) } } // Handle password reset const handlePasswordReset = () => { if (passwordBreach?.resetUrl && userPreview?.email) { window.location.href = `${passwordBreach.resetUrl}?email=${encodeURIComponent(userPreview.email)}`; } } const handleBack = () => { if (step === 'breach-warning') { setStep('password') setPasswordBreach(null) } else { setStep('email') setError('') passwordForm.reset() emailForm.reset() setUserPreview(null) } } const handleOAuthLogin = (provider: OAuthProvider) => { // Create a URL-friendly version of the provider name const providerNameForUrl = provider.name .toLowerCase() .replace(/\s+/g, '') // Remove all whitespace .replace(/[^a-z0-9]/g, '') // Remove any non-alphanumeric characters // Include redirectTo parameter in the OAuth URL window.location.href = `/api/auth/oauth/authorize/${providerNameForUrl}?redirect=${encodeURIComponent(redirectTo)}` } const handleSAMLLogin = (provider: {id: string; name: string; isDefault: boolean}) => { const providerNameForUrl = provider.name .toLowerCase() .replace(/\s+/g, '-') .replace(/[^a-z0-9-]/g, '') window.location.href = `/api/auth/saml/authorize/${providerNameForUrl}?redirect=${encodeURIComponent(redirectTo)}` } const handlePasskeyLogin = async () => { try { setError('') setIsAuthenticating(true) // Get authentication options const optionsResponse = await fetch('/api/auth/passkeys/authenticate/options', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ email: userPreview?.email || emailForm.getValues('email') || undefined }), }) if (!optionsResponse.ok) { throw new Error('Failed to get authentication options') } const {options, challenge} = await optionsResponse.json() // Start WebAuthn authentication const authenticationResponse = await startAuthentication(options) // Verify with server const verifyResponse = await fetch('/api/auth/passkeys/authenticate/verify', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ response: authenticationResponse, challenge, }), }) if (!verifyResponse.ok) { const errorData = await verifyResponse.json() throw new Error(errorData.error || 'Authentication failed') } const verifyData = await verifyResponse.json() // Check if 2FA is required if (verifyData.requiresSecondFactor) { sessionStorage.setItem('2faSessionToken', verifyData.sessionToken) sessionStorage.setItem('2faType', verifyData.secondFactorType) window.location.href = '/two-factor' return } // Success state with confetti setIsSuccess(true) setTimeout(() => { fireConfetti() }, 300) // Redirect with window.location setTimeout(() => { window.location.href = redirectTo }, 1500) } catch (err) { console.error('Passkey login error:', err) setError(err instanceof Error ? err.message : 'Failed to authenticate with passkey') } finally { setIsAuthenticating(false) } } const togglePasswordVisibility = () => { setShowPassword(!showPassword) } if (authLoading) { return (

Loading...

) } return (
{isSuccess ? (

Login Successful

You've signed in successfully. Redirecting you now...

) : (
{step === 'email' ? (

Sign in to Changerawr

Enter your email to get started

{error && ( {error} )}
} />
{emailForm.formState.errors.email && (

{emailForm.formState.errors.email.message}

)}
{/* Auth Options */} {(supportsWebAuthn || (!isLoadingProviders && oauthProviders && oauthProviders.length > 0) || (!isLoadingSAMLProviders && samlProviders && samlProviders.length > 0)) && (
Or continue with
{/* Passkey Button */} {supportsWebAuthn && ( )} {/* OAuth Provider Buttons */} {!isLoadingProviders && oauthProviders && oauthProviders.map((provider: OAuthProvider) => ( ))} {/* SAML Provider Buttons */} {!isLoadingSAMLProviders && samlProviders && samlProviders.map((provider: {id: string; name: string; isDefault: boolean}) => ( ))}
)}
) : step === 'password' ? (

Welcome back{userPreview?.name ? `, ${userPreview.name}` : ''}

{userPreview?.email}

{error && ( {error} )}
Forgot password?
} />
{passwordForm.formState.errors.password && (

{passwordForm.formState.errors.password.message}

)}
{/* Show passkey option in password step too */} {supportsWebAuthn && (
Or
)}
) : step === 'breach-warning' ? (

Password Security Alert

Your password was found in a data breach

} className="border-amber-200 dark:border-amber-800" > Security Notice This password has appeared in{' '} {passwordBreach ? formatBreachCount(passwordBreach.breachCount) : '0'} known data breach{passwordBreach && passwordBreach.breachCount === 1 ? '' : 'es'} . Using it puts your account at risk.

What does this mean?

  • Your password has been compromised in past security incidents
  • Attackers may have access to this password
  • Your account security could be at risk
Or

Password checking powered by{' '} HaveIBeenPwned

Your password is never transmitted - only a secure hash is checked.

) : null}
{step === 'email' && (

Don't have an account?{' '}

You need an invitation to create an account

)}
)}
) } ================================================ FILE: app/(auth)/oauth-callback/layout.tsx ================================================ import { cookies, headers } from 'next/headers' import React from "react"; export default async function OAuthCallbackLayout({ children, }: { children: React.ReactNode }) { const headersList = await headers() const redirectUrl = headersList.get('X-OAuth-Redirect') || '/dashboard' // Get user data from auth context const cookieStore = await cookies() const accessToken = cookieStore.get('accessToken')?.value // This will be serialized and passed to the client component const oauthData = { redirectTo: redirectUrl, hasToken: !!accessToken } return (
{/* Hidden div with the OAuth data for the client component */} ', returnEnd: true, subLanguage: 'javascript' } } ] })); SyntaxHighlighter.registerLanguage('svelte', () => ({ contains: [ { className: 'tag', begin: '', starts: { end: '', returnEnd: true, subLanguage: 'javascript' } }, { className: 'template', begin: '{', end: '}', subLanguage: 'javascript' }, { className: 'tag', begin: '<[A-Za-z]', end: '>', contains: [ { className: 'attr', begin: ' [A-Za-z]+=', end: /(?=\s|$)/, contains: [ { className: 'string', begin: '"', end: '"' } ] } ] } ] })); interface WidgetEditorProps { widget: Widget & { project: { name: string; isPublic: boolean } }; projectId: string; } export default function WidgetEditor({widget: initialWidget, projectId}: WidgetEditorProps) { const router = useRouter(); const {theme} = useTheme(); const [widget, setWidget] = useState(initialWidget); const [saving, setSaving] = useState(false); const [copied, setCopied] = useState(false); const [previewKey, setPreviewKey] = useState(0); const iframeRef = useRef(null); const getEmbedCode = (language: string) => { const scriptUrl = `${window.location.origin}/api/integrations/widget/${projectId}/${widget.id}`; switch (language) { case 'HTML': return ``; case 'React': return `import { useEffect } from 'react'; export default function Changelog() { useEffect(() => { const script = document.createElement('script'); script.src = '${scriptUrl}'; script.async = true; document.body.appendChild(script); return () => { document.body.removeChild(script); }; }, []); return
; }`; case 'Vue': return ` `; case 'Svelte': return `
`; case 'Angular': return `import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-changelog', template: '
' }) export class ChangelogComponent implements OnInit { ngOnInit() { const script = document.createElement('script'); script.src = '${scriptUrl}'; script.async = true; document.body.appendChild(script); } }`; case 'Go': return `package main import "html/template" func ChangelogWidget() template.HTML { return template.HTML(\`\`) }`; default: return ''; } }; const getLanguageForHighlighter = (lang: string) => { switch (lang) { case 'HTML': return 'xml'; case 'React': return 'javascript'; case 'Vue': return 'vue'; case 'Svelte': return 'svelte'; case 'Angular': return 'typescript'; case 'Go': return 'go'; default: return 'javascript'; } }; const handleSave = async () => { setSaving(true); try { const res = await fetch(`/api/integrations/widget/${projectId}/${widget.id}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ name: widget.name, customCSS: widget.customCSS, isActive: widget.isActive, }), }); if (!res.ok) throw new Error('Failed to save'); toast({title: 'Success', description: 'Widget saved successfully'}); router.refresh(); } catch (error) { toast({title: 'Error', description: 'Failed to save widget', variant: 'destructive'}); } finally { setSaving(false); } }; const copyEmbedCode = () => { const code = ``; navigator.clipboard.writeText(code); setCopied(true); toast({title: 'Success', description: 'Embed code copied!'}); setTimeout(() => setCopied(false), 2000); }; const refreshPreview = () => { setPreviewKey(prev => prev + 1); }; const getPreviewHTML = () => { const scriptUrl = `${window.location.origin}/api/integrations/widget/${projectId}/${widget.id}`; // Different preview layouts based on variant const variantSpecificContent = { classic: `

Widget Preview

`, floating: `

Your Website Content

The floating widget appears in the corner →

`, modal: `

Modal Widget Preview

Click the button below to open the modal

`, announcement: `

Announcement Bar Preview

The announcement bar appears at the top of the page ↑

` }; const content = variantSpecificContent[widget.variant as keyof typeof variantSpecificContent] || variantSpecificContent.classic; return ` Widget Preview ${content} `; }; useEffect(() => { if (iframeRef.current) { const doc = iframeRef.current.contentDocument; if (doc) { doc.open(); doc.write(getPreviewHTML()); doc.close(); } } }, [previewKey, widget.id, widget.variant, theme]); return (

Edit Widget

{widget.variant} variant

Widget Settings Configure your widget
setWidget({...widget, name: e.target.value})} />

Variant cannot be changed after creation

Inactive widgets won't load on your site

setWidget({...widget, isActive: checked}) } />