@commonpub/layer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/app.vue +7 -0
- package/components/AnnouncementBand.vue +117 -0
- package/components/AppToast.vue +108 -0
- package/components/AuthorCard.vue +119 -0
- package/components/AuthorRow.vue +81 -0
- package/components/CommentSection.vue +330 -0
- package/components/ContentCard.vue +340 -0
- package/components/ContentPicker.vue +240 -0
- package/components/ContentStarterForm.vue +214 -0
- package/components/ContentTypeBadge.vue +46 -0
- package/components/CountdownTimer.vue +68 -0
- package/components/CpubEditor.vue +87 -0
- package/components/DiscussionItem.vue +191 -0
- package/components/EditorPropertiesPanel.vue +393 -0
- package/components/EngagementBar.vue +131 -0
- package/components/FederatedContentCard.vue +291 -0
- package/components/FeedItem.vue +283 -0
- package/components/FilterChip.vue +21 -0
- package/components/HeatmapGrid.vue +92 -0
- package/components/ImageUpload.vue +219 -0
- package/components/MemberCard.vue +163 -0
- package/components/MessageThread.vue +120 -0
- package/components/NotificationItem.vue +103 -0
- package/components/ProgressTracker.vue +41 -0
- package/components/PublishErrorsModal.vue +116 -0
- package/components/RemoteActorCard.vue +206 -0
- package/components/RemoteUserSearch.vue +117 -0
- package/components/SearchFilters.vue +188 -0
- package/components/SearchSidebar.vue +181 -0
- package/components/SectionHeader.vue +17 -0
- package/components/ShareToHubModal.vue +189 -0
- package/components/SiteLogo.vue +21 -0
- package/components/SkillBar.vue +57 -0
- package/components/SortSelect.vue +30 -0
- package/components/StatBar.vue +14 -0
- package/components/TOCNav.vue +69 -0
- package/components/TimelineItem.vue +82 -0
- package/components/VideoCard.vue +106 -0
- package/components/blocks/BlockBuildStepView.vue +92 -0
- package/components/blocks/BlockCalloutView.vue +82 -0
- package/components/blocks/BlockCheckpointView.vue +50 -0
- package/components/blocks/BlockCodeView.vue +212 -0
- package/components/blocks/BlockContentRenderer.vue +143 -0
- package/components/blocks/BlockDividerView.vue +11 -0
- package/components/blocks/BlockDownloadsView.vue +126 -0
- package/components/blocks/BlockEmbedView.vue +61 -0
- package/components/blocks/BlockGalleryView.vue +57 -0
- package/components/blocks/BlockHeadingView.vue +29 -0
- package/components/blocks/BlockImageView.vue +34 -0
- package/components/blocks/BlockMarkdownView.vue +118 -0
- package/components/blocks/BlockMathView.vue +45 -0
- package/components/blocks/BlockPartsListView.vue +104 -0
- package/components/blocks/BlockQuizView.vue +239 -0
- package/components/blocks/BlockQuoteView.vue +41 -0
- package/components/blocks/BlockSectionHeaderView.vue +58 -0
- package/components/blocks/BlockSliderView.vue +236 -0
- package/components/blocks/BlockTextView.vue +41 -0
- package/components/blocks/BlockToolListView.vue +87 -0
- package/components/blocks/BlockVideoView.vue +89 -0
- package/components/editors/ArticleEditor.vue +545 -0
- package/components/editors/BlockCanvas.vue +487 -0
- package/components/editors/BlockInsertZone.vue +84 -0
- package/components/editors/BlockPicker.vue +285 -0
- package/components/editors/BlockWrapper.vue +192 -0
- package/components/editors/BlogEditor.vue +567 -0
- package/components/editors/EditorBlocks.vue +248 -0
- package/components/editors/EditorSection.vue +81 -0
- package/components/editors/EditorShell.vue +168 -0
- package/components/editors/EditorTagInput.vue +114 -0
- package/components/editors/EditorVisibility.vue +110 -0
- package/components/editors/ExplainerEditor.vue +503 -0
- package/components/editors/MarkdownImportDialog.vue +249 -0
- package/components/editors/ProjectEditor.vue +446 -0
- package/components/editors/blocks/BuildStepBlock.vue +102 -0
- package/components/editors/blocks/CalloutBlock.vue +122 -0
- package/components/editors/blocks/CheckpointBlock.vue +27 -0
- package/components/editors/blocks/CodeBlock.vue +177 -0
- package/components/editors/blocks/DividerBlock.vue +22 -0
- package/components/editors/blocks/DownloadsBlock.vue +41 -0
- package/components/editors/blocks/EmbedBlock.vue +20 -0
- package/components/editors/blocks/GalleryBlock.vue +236 -0
- package/components/editors/blocks/HeadingBlock.vue +96 -0
- package/components/editors/blocks/ImageBlock.vue +271 -0
- package/components/editors/blocks/MarkdownBlock.vue +258 -0
- package/components/editors/blocks/MathBlock.vue +37 -0
- package/components/editors/blocks/PartsListBlock.vue +358 -0
- package/components/editors/blocks/QuizBlock.vue +47 -0
- package/components/editors/blocks/QuoteBlock.vue +101 -0
- package/components/editors/blocks/SectionHeaderBlock.vue +130 -0
- package/components/editors/blocks/SliderBlock.vue +318 -0
- package/components/editors/blocks/TextBlock.vue +201 -0
- package/components/editors/blocks/ToolListBlock.vue +70 -0
- package/components/editors/blocks/VideoBlock.vue +22 -0
- package/components/hub/HubDiscussions.vue +47 -0
- package/components/hub/HubFeed.vue +199 -0
- package/components/hub/HubHero.vue +185 -0
- package/components/hub/HubLayout.vue +103 -0
- package/components/hub/HubMembers.vue +40 -0
- package/components/hub/HubProducts.vue +93 -0
- package/components/hub/HubProjects.vue +207 -0
- package/components/hub/HubSidebar.vue +12 -0
- package/components/hub/HubSidebarCard.vue +35 -0
- package/components/views/ArticleView.vue +771 -0
- package/components/views/BlogView.vue +667 -0
- package/components/views/ExplainerView.vue +688 -0
- package/components/views/ProjectView.vue +1500 -0
- package/composables/useApiError.ts +39 -0
- package/composables/useAuth.ts +87 -0
- package/composables/useBlockEditor.ts +187 -0
- package/composables/useContentSave.ts +253 -0
- package/composables/useContentTypes.ts +37 -0
- package/composables/useEngagement.ts +196 -0
- package/composables/useFeatures.ts +33 -0
- package/composables/useFederation.ts +72 -0
- package/composables/useJsonLd.ts +183 -0
- package/composables/useMarkdownImport.ts +77 -0
- package/composables/useMirrorContent.ts +105 -0
- package/composables/useNotifications.ts +73 -0
- package/composables/usePublishValidation.ts +65 -0
- package/composables/useSanitize.ts +34 -0
- package/composables/useSiteName.ts +4 -0
- package/composables/useTheme.ts +34 -0
- package/composables/useToast.ts +35 -0
- package/error.vue +129 -0
- package/layouts/admin.vue +213 -0
- package/layouts/auth.vue +63 -0
- package/layouts/default.vue +269 -0
- package/layouts/editor.vue +129 -0
- package/middleware/auth.ts +6 -0
- package/nuxt.config.ts +83 -0
- package/package.json +59 -0
- package/pages/[type]/[slug]/edit.vue +676 -0
- package/pages/[type]/[slug]/index.vue +313 -0
- package/pages/[type]/index.vue +118 -0
- package/pages/about.vue +100 -0
- package/pages/admin/audit.vue +66 -0
- package/pages/admin/content.vue +116 -0
- package/pages/admin/federation.vue +446 -0
- package/pages/admin/index.vue +62 -0
- package/pages/admin/reports.vue +88 -0
- package/pages/admin/settings.vue +167 -0
- package/pages/admin/users.vue +145 -0
- package/pages/auth/forgot-password.vue +103 -0
- package/pages/auth/login.vue +216 -0
- package/pages/auth/oauth/authorize.vue +178 -0
- package/pages/auth/register.vue +246 -0
- package/pages/auth/reset-password.vue +124 -0
- package/pages/auth/verify-email.vue +80 -0
- package/pages/cert/[code].vue +154 -0
- package/pages/contests/[slug]/edit.vue +153 -0
- package/pages/contests/[slug]/index.vue +556 -0
- package/pages/contests/[slug]/judge.vue +160 -0
- package/pages/contests/create.vue +192 -0
- package/pages/contests/index.vue +69 -0
- package/pages/create.vue +219 -0
- package/pages/dashboard.vue +521 -0
- package/pages/docs/[siteSlug]/[...pagePath].vue +704 -0
- package/pages/docs/[siteSlug]/edit.vue +480 -0
- package/pages/docs/[siteSlug]/index.vue +380 -0
- package/pages/docs/create.vue +60 -0
- package/pages/docs/index.vue +181 -0
- package/pages/explore.vue +422 -0
- package/pages/federated-hubs/[id]/index.vue +385 -0
- package/pages/federated-hubs/[id]/posts/[postId].vue +309 -0
- package/pages/federation/index.vue +157 -0
- package/pages/federation/search.vue +43 -0
- package/pages/federation/users/[handle].vue +221 -0
- package/pages/feed.vue +135 -0
- package/pages/hubs/[slug]/index.vue +513 -0
- package/pages/hubs/[slug]/members.vue +134 -0
- package/pages/hubs/[slug]/posts/[postId].vue +352 -0
- package/pages/hubs/[slug]/settings.vue +254 -0
- package/pages/hubs/create.vue +201 -0
- package/pages/hubs/index.vue +207 -0
- package/pages/index.vue +1005 -0
- package/pages/learn/[slug]/[lessonSlug]/edit.vue +413 -0
- package/pages/learn/[slug]/[lessonSlug]/index.vue +438 -0
- package/pages/learn/[slug]/edit.vue +414 -0
- package/pages/learn/[slug]/index.vue +341 -0
- package/pages/learn/create.vue +71 -0
- package/pages/learn/index.vue +360 -0
- package/pages/messages/[conversationId].vue +113 -0
- package/pages/messages/index.vue +303 -0
- package/pages/mirror/[id].vue +115 -0
- package/pages/notifications.vue +91 -0
- package/pages/products/[slug].vue +128 -0
- package/pages/products/index.vue +122 -0
- package/pages/search.vue +692 -0
- package/pages/settings/account.vue +170 -0
- package/pages/settings/appearance.vue +80 -0
- package/pages/settings/index.vue +81 -0
- package/pages/settings/notifications.vue +68 -0
- package/pages/settings/profile.vue +838 -0
- package/pages/tags/[slug].vue +111 -0
- package/pages/tags/index.vue +73 -0
- package/pages/u/[username]/followers.vue +86 -0
- package/pages/u/[username]/following.vue +94 -0
- package/pages/u/[username]/index.vue +837 -0
- package/pages/videos/[id].vue +212 -0
- package/pages/videos/index.vue +327 -0
- package/pages/videos/submit.vue +112 -0
- package/plugins/auth.ts +23 -0
- package/server/api/admin/audit.get.ts +17 -0
- package/server/api/admin/content/[id].delete.ts +15 -0
- package/server/api/admin/content/[id].patch.ts +37 -0
- package/server/api/admin/federation/activity.get.ts +31 -0
- package/server/api/admin/federation/clients.get.ts +9 -0
- package/server/api/admin/federation/clients.post.ts +16 -0
- package/server/api/admin/federation/hub-mirrors/index.get.ts +10 -0
- package/server/api/admin/federation/hub-mirrors/index.post.ts +42 -0
- package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +39 -0
- package/server/api/admin/federation/mirrors/[id].delete.ts +11 -0
- package/server/api/admin/federation/mirrors/[id].get.ts +15 -0
- package/server/api/admin/federation/mirrors/[id].put.ts +22 -0
- package/server/api/admin/federation/mirrors/index.get.ts +9 -0
- package/server/api/admin/federation/mirrors/index.post.ts +30 -0
- package/server/api/admin/federation/pending.get.ts +22 -0
- package/server/api/admin/federation/refederate.post.ts +92 -0
- package/server/api/admin/federation/repair-types.post.ts +57 -0
- package/server/api/admin/federation/retry.post.ts +41 -0
- package/server/api/admin/federation/stats.get.ts +26 -0
- package/server/api/admin/reports/[id]/resolve.post.ts +12 -0
- package/server/api/admin/reports.get.ts +17 -0
- package/server/api/admin/settings.get.ts +22 -0
- package/server/api/admin/settings.put.ts +11 -0
- package/server/api/admin/stats.get.ts +9 -0
- package/server/api/admin/users/[id]/role.put.ts +12 -0
- package/server/api/admin/users/[id]/status.put.ts +12 -0
- package/server/api/admin/users/[id].delete.ts +10 -0
- package/server/api/admin/users.get.ts +18 -0
- package/server/api/auth/federated/callback.get.ts +67 -0
- package/server/api/auth/federated/login.post.ts +60 -0
- package/server/api/auth/oauth2/authorize.get.ts +30 -0
- package/server/api/auth/oauth2/authorize.post.ts +51 -0
- package/server/api/auth/oauth2/register.post.ts +41 -0
- package/server/api/auth/oauth2/token.post.ts +48 -0
- package/server/api/cert/[code].get.ts +13 -0
- package/server/api/content/[id]/build.post.ts +9 -0
- package/server/api/content/[id]/fork.post.ts +10 -0
- package/server/api/content/[id]/index.delete.ts +18 -0
- package/server/api/content/[id]/index.get.ts +15 -0
- package/server/api/content/[id]/index.put.ts +23 -0
- package/server/api/content/[id]/products/[productId].delete.ts +28 -0
- package/server/api/content/[id]/products-sync.post.ts +29 -0
- package/server/api/content/[id]/products.get.ts +9 -0
- package/server/api/content/[id]/products.post.ts +31 -0
- package/server/api/content/[id]/publish.post.ts +17 -0
- package/server/api/content/[id]/report.post.ts +17 -0
- package/server/api/content/[id]/versions.get.ts +9 -0
- package/server/api/content/[id]/view.post.ts +34 -0
- package/server/api/content/index.get.ts +23 -0
- package/server/api/content/index.post.ts +11 -0
- package/server/api/contests/[slug]/entries.get.ts +18 -0
- package/server/api/contests/[slug]/entries.post.ts +23 -0
- package/server/api/contests/[slug]/index.delete.ts +21 -0
- package/server/api/contests/[slug]/index.get.ts +11 -0
- package/server/api/contests/[slug]/index.put.ts +15 -0
- package/server/api/contests/[slug]/judge.post.ts +12 -0
- package/server/api/contests/[slug]/transition.post.ts +24 -0
- package/server/api/contests/index.get.ts +10 -0
- package/server/api/contests/index.post.ts +28 -0
- package/server/api/docs/[siteSlug]/index.delete.ts +14 -0
- package/server/api/docs/[siteSlug]/index.get.ts +17 -0
- package/server/api/docs/[siteSlug]/index.put.ts +20 -0
- package/server/api/docs/[siteSlug]/nav.get.ts +26 -0
- package/server/api/docs/[siteSlug]/pages/[pageId].delete.ts +14 -0
- package/server/api/docs/[siteSlug]/pages/[pageId].get.ts +31 -0
- package/server/api/docs/[siteSlug]/pages/[pageId].put.ts +15 -0
- package/server/api/docs/[siteSlug]/pages/index.get.ts +34 -0
- package/server/api/docs/[siteSlug]/pages/index.post.ts +28 -0
- package/server/api/docs/[siteSlug]/pages/reorder.post.ts +26 -0
- package/server/api/docs/[siteSlug]/search.get.ts +20 -0
- package/server/api/docs/[siteSlug]/versions.post.ts +11 -0
- package/server/api/docs/index.get.ts +6 -0
- package/server/api/docs/index.post.ts +10 -0
- package/server/api/federated-hubs/[id]/posts/[postId].get.ts +16 -0
- package/server/api/federated-hubs/[id]/posts.get.ts +15 -0
- package/server/api/federated-hubs/[id].get.ts +16 -0
- package/server/api/federation/boost.post.ts +21 -0
- package/server/api/federation/content/[id].get.ts +15 -0
- package/server/api/federation/follow.post.ts +16 -0
- package/server/api/federation/health.get.ts +56 -0
- package/server/api/federation/hub-follow.post.ts +27 -0
- package/server/api/federation/hub-post-like.post.ts +115 -0
- package/server/api/federation/hub-post-likes.get.ts +24 -0
- package/server/api/federation/hub-post-reply.post.ts +42 -0
- package/server/api/federation/hub-post.post.ts +33 -0
- package/server/api/federation/like.post.ts +21 -0
- package/server/api/federation/remote-actor.get.ts +22 -0
- package/server/api/federation/reply.post.ts +22 -0
- package/server/api/federation/search.post.ts +17 -0
- package/server/api/federation/timeline.get.ts +22 -0
- package/server/api/federation/unfollow.post.ts +17 -0
- package/server/api/files/[id].delete.ts +35 -0
- package/server/api/files/mine.get.ts +31 -0
- package/server/api/files/upload-from-url.post.ts +68 -0
- package/server/api/files/upload.post.ts +105 -0
- package/server/api/health.get.ts +4 -0
- package/server/api/hubs/[slug]/bans/[userId].delete.ts +13 -0
- package/server/api/hubs/[slug]/bans.get.ts +20 -0
- package/server/api/hubs/[slug]/bans.post.ts +23 -0
- package/server/api/hubs/[slug]/feed.xml.get.ts +60 -0
- package/server/api/hubs/[slug]/gallery.get.ts +21 -0
- package/server/api/hubs/[slug]/index.delete.ts +18 -0
- package/server/api/hubs/[slug]/index.get.ts +14 -0
- package/server/api/hubs/[slug]/index.put.ts +22 -0
- package/server/api/hubs/[slug]/invites.get.ts +20 -0
- package/server/api/hubs/[slug]/invites.post.ts +27 -0
- package/server/api/hubs/[slug]/join.post.ts +17 -0
- package/server/api/hubs/[slug]/leave.post.ts +13 -0
- package/server/api/hubs/[slug]/members/[userId].delete.ts +13 -0
- package/server/api/hubs/[slug]/members/[userId].put.ts +16 -0
- package/server/api/hubs/[slug]/members.get.ts +20 -0
- package/server/api/hubs/[slug]/posts/[postId]/like.post.ts +21 -0
- package/server/api/hubs/[slug]/posts/[postId]/lock.post.ts +17 -0
- package/server/api/hubs/[slug]/posts/[postId]/pin.post.ts +17 -0
- package/server/api/hubs/[slug]/posts/[postId]/replies.get.ts +16 -0
- package/server/api/hubs/[slug]/posts/[postId]/replies.post.ts +21 -0
- package/server/api/hubs/[slug]/posts/[postId].delete.ts +23 -0
- package/server/api/hubs/[slug]/posts/[postId].get.ts +16 -0
- package/server/api/hubs/[slug]/posts/index.get.ts +16 -0
- package/server/api/hubs/[slug]/posts/index.post.ts +36 -0
- package/server/api/hubs/[slug]/products.get.ts +26 -0
- package/server/api/hubs/[slug]/products.post.ts +18 -0
- package/server/api/hubs/[slug]/share.post.ts +39 -0
- package/server/api/hubs/index.get.ts +15 -0
- package/server/api/hubs/index.post.ts +11 -0
- package/server/api/image-proxy.get.ts +91 -0
- package/server/api/learn/[slug]/[lessonSlug]/complete.post.ts +13 -0
- package/server/api/learn/[slug]/[lessonSlug]/index.get.ts +68 -0
- package/server/api/learn/[slug]/enroll.post.ts +12 -0
- package/server/api/learn/[slug]/index.delete.ts +12 -0
- package/server/api/learn/[slug]/index.get.ts +14 -0
- package/server/api/learn/[slug]/index.put.ts +19 -0
- package/server/api/learn/[slug]/lessons/[lessonId].delete.ts +14 -0
- package/server/api/learn/[slug]/lessons/[lessonId].put.ts +24 -0
- package/server/api/learn/[slug]/lessons.post.ts +10 -0
- package/server/api/learn/[slug]/modules/[moduleId].delete.ts +14 -0
- package/server/api/learn/[slug]/modules/[moduleId].put.ts +15 -0
- package/server/api/learn/[slug]/modules.post.ts +14 -0
- package/server/api/learn/[slug]/publish.post.ts +13 -0
- package/server/api/learn/[slug]/unenroll.post.ts +12 -0
- package/server/api/learn/certificates.get.ts +9 -0
- package/server/api/learn/enrollments.get.ts +9 -0
- package/server/api/learn/index.get.ts +17 -0
- package/server/api/learn/index.post.ts +11 -0
- package/server/api/me.get.ts +13 -0
- package/server/api/messages/[conversationId]/info.get.ts +43 -0
- package/server/api/messages/[conversationId]/stream.get.ts +73 -0
- package/server/api/messages/[conversationId].get.ts +13 -0
- package/server/api/messages/[conversationId].post.ts +12 -0
- package/server/api/messages/index.get.ts +39 -0
- package/server/api/messages/index.post.ts +58 -0
- package/server/api/notifications/[id].delete.ts +11 -0
- package/server/api/notifications/count.get.ts +10 -0
- package/server/api/notifications/index.get.ts +24 -0
- package/server/api/notifications/read.post.ts +15 -0
- package/server/api/notifications/stream.get.ts +59 -0
- package/server/api/openapi.get.ts +5 -0
- package/server/api/products/[id].delete.ts +15 -0
- package/server/api/products/[id].put.ts +18 -0
- package/server/api/products/[slug]/content.get.ts +21 -0
- package/server/api/products/[slug].get.ts +16 -0
- package/server/api/products/index.get.ts +28 -0
- package/server/api/profile.get.ts +15 -0
- package/server/api/profile.put.ts +24 -0
- package/server/api/resolve-identity.post.ts +34 -0
- package/server/api/search/index.get.ts +24 -0
- package/server/api/search/trending.get.ts +22 -0
- package/server/api/social/bookmark.get.ts +16 -0
- package/server/api/social/bookmark.post.ts +15 -0
- package/server/api/social/bookmarks.get.ts +16 -0
- package/server/api/social/comments/[id].delete.ts +13 -0
- package/server/api/social/comments.get.ts +18 -0
- package/server/api/social/comments.post.ts +11 -0
- package/server/api/social/like.get.ts +17 -0
- package/server/api/social/like.post.ts +35 -0
- package/server/api/stats.get.ts +8 -0
- package/server/api/user/hubs.get.ts +22 -0
- package/server/api/users/[username]/content.get.ts +21 -0
- package/server/api/users/[username]/feed.xml.get.ts +65 -0
- package/server/api/users/[username]/follow.delete.ts +15 -0
- package/server/api/users/[username]/follow.post.ts +15 -0
- package/server/api/users/[username]/followers.get.ts +22 -0
- package/server/api/users/[username]/following.get.ts +22 -0
- package/server/api/users/[username]/learning.get.ts +18 -0
- package/server/api/users/[username].get.ts +25 -0
- package/server/api/users/index.get.ts +78 -0
- package/server/api/videos/[id].get.ts +18 -0
- package/server/api/videos/categories/[id].delete.ts +15 -0
- package/server/api/videos/categories/[id].put.ts +17 -0
- package/server/api/videos/categories.get.ts +7 -0
- package/server/api/videos/categories.post.ts +11 -0
- package/server/api/videos/index.get.ts +20 -0
- package/server/api/videos/index.post.ts +11 -0
- package/server/middleware/auth.ts +180 -0
- package/server/middleware/features.ts +31 -0
- package/server/middleware/security.ts +54 -0
- package/server/plugins/auto-admin.ts +69 -0
- package/server/plugins/federation-delivery.ts +74 -0
- package/server/routes/.well-known/nodeinfo.ts +15 -0
- package/server/routes/.well-known/webfinger.ts +86 -0
- package/server/routes/actor/followers.ts +34 -0
- package/server/routes/actor/following.ts +34 -0
- package/server/routes/actor/outbox.ts +31 -0
- package/server/routes/actor.ts +30 -0
- package/server/routes/feed.xml.ts +59 -0
- package/server/routes/hubs/[slug]/followers.ts +39 -0
- package/server/routes/hubs/[slug]/inbox.ts +35 -0
- package/server/routes/hubs/[slug]/outbox.ts +43 -0
- package/server/routes/hubs/[slug]/posts/[postId].ts +63 -0
- package/server/routes/hubs/[slug].ts +29 -0
- package/server/routes/inbox.ts +34 -0
- package/server/routes/nodeinfo/2.1.ts +27 -0
- package/server/routes/robots.txt.ts +19 -0
- package/server/routes/sitemap.xml.ts +105 -0
- package/server/routes/users/[username]/followers.ts +33 -0
- package/server/routes/users/[username]/following.ts +33 -0
- package/server/routes/users/[username]/inbox.ts +34 -0
- package/server/routes/users/[username]/outbox.ts +35 -0
- package/server/routes/users/[username].ts +62 -0
- package/server/utils/auth.ts +36 -0
- package/server/utils/db.ts +34 -0
- package/server/utils/errors.ts +24 -0
- package/server/utils/inbox.ts +122 -0
- package/server/utils/validate.ts +82 -0
- package/theme/base.css +283 -0
- package/theme/components.css +322 -0
- package/theme/dark.css +87 -0
- package/theme/editor-panels.css +63 -0
- package/theme/forms.css +216 -0
- package/theme/generics.css +68 -0
- package/theme/layouts.css +415 -0
- package/theme/prose.css +342 -0
- package/types/hub.ts +61 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/image-proxy?url=<remote-url>&w=<width>
|
|
3
|
+
* Proxies and caches remote images for federated content.
|
|
4
|
+
* Prevents slow cross-origin fetches on content cards.
|
|
5
|
+
*
|
|
6
|
+
* Security: only proxies images from known federation origins
|
|
7
|
+
* (federated_content.origin_domain or remote_actors.instance_domain).
|
|
8
|
+
*/
|
|
9
|
+
export default defineEventHandler(async (event) => {
|
|
10
|
+
const query = getQuery(event);
|
|
11
|
+
const url = query.url as string | undefined;
|
|
12
|
+
const width = Math.min(parseInt(String(query.w || '800'), 10), 1920);
|
|
13
|
+
|
|
14
|
+
if (!url || typeof url !== 'string') {
|
|
15
|
+
throw createError({ statusCode: 400, statusMessage: 'Missing url parameter' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Parse and validate the URL
|
|
19
|
+
let parsed: URL;
|
|
20
|
+
try {
|
|
21
|
+
parsed = new URL(url);
|
|
22
|
+
} catch {
|
|
23
|
+
throw createError({ statusCode: 400, statusMessage: 'Invalid URL' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Only allow HTTPS image URLs
|
|
27
|
+
if (parsed.protocol !== 'https:') {
|
|
28
|
+
throw createError({ statusCode: 400, statusMessage: 'Only HTTPS URLs allowed' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Block localhost/private IPs (SSRF prevention)
|
|
32
|
+
const hostname = parsed.hostname;
|
|
33
|
+
if (
|
|
34
|
+
hostname === 'localhost' ||
|
|
35
|
+
hostname === '127.0.0.1' ||
|
|
36
|
+
hostname === '::1' ||
|
|
37
|
+
hostname.startsWith('10.') ||
|
|
38
|
+
hostname.startsWith('172.') ||
|
|
39
|
+
hostname.startsWith('192.168.') ||
|
|
40
|
+
hostname.endsWith('.local')
|
|
41
|
+
) {
|
|
42
|
+
throw createError({ statusCode: 403, statusMessage: 'Private addresses not allowed' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fetch the remote image
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const response = await fetch(url, {
|
|
51
|
+
signal: controller.signal,
|
|
52
|
+
headers: {
|
|
53
|
+
Accept: 'image/*',
|
|
54
|
+
'User-Agent': 'CommonPub/1.0 (image-proxy)',
|
|
55
|
+
},
|
|
56
|
+
redirect: 'follow',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
clearTimeout(timeout);
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw createError({ statusCode: 502, statusMessage: `Upstream returned ${response.status}` });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const contentType = response.headers.get('content-type') || '';
|
|
66
|
+
if (!contentType.startsWith('image/')) {
|
|
67
|
+
throw createError({ statusCode: 502, statusMessage: 'Not an image' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Limit to 10MB
|
|
71
|
+
const contentLength = parseInt(response.headers.get('content-length') || '0', 10);
|
|
72
|
+
if (contentLength > 10 * 1024 * 1024) {
|
|
73
|
+
throw createError({ statusCode: 502, statusMessage: 'Image too large' });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
77
|
+
|
|
78
|
+
// Set aggressive cache headers — federated images rarely change
|
|
79
|
+
setResponseHeaders(event, {
|
|
80
|
+
'Content-Type': contentType,
|
|
81
|
+
'Cache-Control': 'public, max-age=86400, s-maxage=604800, stale-while-revalidate=86400',
|
|
82
|
+
'X-Image-Proxy': 'commonpub',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return buffer;
|
|
86
|
+
} catch (err: unknown) {
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
if ((err as { statusCode?: number })?.statusCode) throw err;
|
|
89
|
+
throw createError({ statusCode: 502, statusMessage: 'Failed to fetch image' });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getLessonBySlug, markLessonComplete } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { slug, lessonSlug } = parseParams(event, { slug: 'string', lessonSlug: 'string' });
|
|
7
|
+
const body = await readBody(event).catch(() => ({}));
|
|
8
|
+
|
|
9
|
+
const result = await getLessonBySlug(db, slug, lessonSlug);
|
|
10
|
+
if (!result) throw createError({ statusCode: 404, statusMessage: 'Lesson not found' });
|
|
11
|
+
|
|
12
|
+
return markLessonComplete(db, user.id, result.lesson.id, body?.quizScore, body?.quizPassed);
|
|
13
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getLessonBySlug } from '@commonpub/server';
|
|
2
|
+
import { renderMarkdown } from '@commonpub/docs';
|
|
3
|
+
|
|
4
|
+
function blocksToHtml(blocks: unknown): string {
|
|
5
|
+
if (!Array.isArray(blocks)) return '';
|
|
6
|
+
const parts: string[] = [];
|
|
7
|
+
for (const block of blocks) {
|
|
8
|
+
const [type, data] = block as [string, Record<string, unknown>];
|
|
9
|
+
if (!data) continue;
|
|
10
|
+
if (typeof data.html === 'string' && data.html) {
|
|
11
|
+
parts.push(data.html);
|
|
12
|
+
} else if (type === 'heading' && typeof data.text === 'string') {
|
|
13
|
+
const level = Math.min(Math.max(Number(data.level) || 2, 1), 6);
|
|
14
|
+
const escaped = data.text.replace(/</g, '<').replace(/>/g, '>');
|
|
15
|
+
parts.push(`<h${level}>${escaped}</h${level}>`);
|
|
16
|
+
} else if (type === 'code_block' && typeof data.code === 'string') {
|
|
17
|
+
const lang = String(data.language || '').replace(/[^a-zA-Z0-9-]/g, '');
|
|
18
|
+
const escaped = data.code.replace(/</g, '<').replace(/>/g, '>');
|
|
19
|
+
parts.push(`<pre><code class="language-${lang}">${escaped}</code></pre>`);
|
|
20
|
+
} else if (type === 'image' && (data.src || data.url)) {
|
|
21
|
+
const src = String(data.src || data.url).replace(/"/g, '"');
|
|
22
|
+
const alt = String(data.alt || '').replace(/"/g, '"');
|
|
23
|
+
if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('/')) {
|
|
24
|
+
parts.push(`<figure><img src="${src}" alt="${alt}" /></figure>`);
|
|
25
|
+
}
|
|
26
|
+
} else if (type === 'blockquote' && typeof data.text === 'string') {
|
|
27
|
+
const escaped = data.text.replace(/</g, '<').replace(/>/g, '>');
|
|
28
|
+
parts.push(`<blockquote>${escaped}</blockquote>`);
|
|
29
|
+
} else if (type === 'callout' && typeof data.text === 'string') {
|
|
30
|
+
const escaped = data.text.replace(/</g, '<').replace(/>/g, '>');
|
|
31
|
+
parts.push(`<div class="cpub-callout">${escaped}</div>`);
|
|
32
|
+
} else if (type === 'horizontal_rule' || type === 'divider') {
|
|
33
|
+
parts.push('<hr />');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return parts.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default defineEventHandler(async (event) => {
|
|
40
|
+
const db = useDB();
|
|
41
|
+
const { slug, lessonSlug } = parseParams(event, { slug: 'string', lessonSlug: 'string' });
|
|
42
|
+
|
|
43
|
+
const result = await getLessonBySlug(db, slug, lessonSlug);
|
|
44
|
+
if (!result) {
|
|
45
|
+
throw createError({ statusCode: 404, statusMessage: 'Lesson not found' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let renderedHtml = '';
|
|
49
|
+
|
|
50
|
+
if (result.linkedContent?.content) {
|
|
51
|
+
// Linked content uses BlockTuple[] format
|
|
52
|
+
renderedHtml = blocksToHtml(result.linkedContent.content);
|
|
53
|
+
} else {
|
|
54
|
+
// Inline content: render markdown
|
|
55
|
+
const content = result.lesson.content as Record<string, unknown> | null;
|
|
56
|
+
if (content) {
|
|
57
|
+
const md = typeof content.markdown === 'string' ? content.markdown
|
|
58
|
+
: typeof content.notes === 'string' ? content.notes
|
|
59
|
+
: '';
|
|
60
|
+
if (md) {
|
|
61
|
+
const rendered = await renderMarkdown(md);
|
|
62
|
+
renderedHtml = rendered.html;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { ...result, renderedHtml };
|
|
68
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { enroll, getPathBySlug } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { slug } = parseParams(event, { slug: 'string' });
|
|
7
|
+
|
|
8
|
+
const path = await getPathBySlug(db, slug, user.id);
|
|
9
|
+
if (!path) throw createError({ statusCode: 404, statusMessage: 'Path not found' });
|
|
10
|
+
|
|
11
|
+
return enroll(db, user.id, path.id);
|
|
12
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { deletePath, getPathBySlug } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<boolean> => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { slug } = parseParams(event, { slug: 'string' });
|
|
7
|
+
|
|
8
|
+
const path = await getPathBySlug(db, slug, user.id);
|
|
9
|
+
if (!path) throw createError({ statusCode: 404, statusMessage: 'Path not found' });
|
|
10
|
+
|
|
11
|
+
return deletePath(db, path.id, user.id);
|
|
12
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getPathBySlug } from '@commonpub/server';
|
|
2
|
+
import type { LearningPathDetail } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event): Promise<LearningPathDetail> => {
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { slug } = parseParams(event, { slug: 'string' });
|
|
7
|
+
const user = getOptionalUser(event);
|
|
8
|
+
|
|
9
|
+
const path = await getPathBySlug(db, slug, user?.id);
|
|
10
|
+
if (!path) {
|
|
11
|
+
throw createError({ statusCode: 404, statusMessage: 'Learning path not found' });
|
|
12
|
+
}
|
|
13
|
+
return path;
|
|
14
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getPathBySlug, updatePath } from '@commonpub/server';
|
|
2
|
+
import type { LearningPathDetail } from '@commonpub/server';
|
|
3
|
+
import { updateLearningPathSchema } from '@commonpub/schema';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event): Promise<LearningPathDetail> => {
|
|
6
|
+
const user = requireAuth(event);
|
|
7
|
+
const db = useDB();
|
|
8
|
+
const { slug } = parseParams(event, { slug: 'string' });
|
|
9
|
+
const input = await parseBody(event, updateLearningPathSchema);
|
|
10
|
+
|
|
11
|
+
const path = await getPathBySlug(db, slug, user.id);
|
|
12
|
+
if (!path) throw createError({ statusCode: 404, statusMessage: 'Path not found' });
|
|
13
|
+
|
|
14
|
+
const updated = await updatePath(db, path.id, user.id, input);
|
|
15
|
+
if (!updated) {
|
|
16
|
+
throw createError({ statusCode: 403, statusMessage: 'Not authorized to update this path' });
|
|
17
|
+
}
|
|
18
|
+
return updated;
|
|
19
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { deleteLesson } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { lessonId } = parseParams(event, { lessonId: 'uuid' });
|
|
7
|
+
|
|
8
|
+
const deleted = await deleteLesson(db, lessonId, user.id);
|
|
9
|
+
if (!deleted) {
|
|
10
|
+
throw createError({ statusCode: 404, statusMessage: 'Lesson not found or not authorized' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return { success: true };
|
|
14
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { updateLesson } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const updateLessonSchema = z.object({
|
|
5
|
+
title: z.string().min(1).max(255).optional(),
|
|
6
|
+
type: z.enum(['article', 'video', 'quiz', 'project', 'explainer']).optional(),
|
|
7
|
+
content: z.unknown().optional(),
|
|
8
|
+
contentItemId: z.string().uuid().nullable().optional(),
|
|
9
|
+
durationMinutes: z.number().int().min(0).max(9999).optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export default defineEventHandler(async (event) => {
|
|
13
|
+
const db = useDB();
|
|
14
|
+
const user = requireAuth(event);
|
|
15
|
+
const { lessonId } = parseParams(event, { lessonId: 'uuid' });
|
|
16
|
+
const input = await parseBody(event, updateLessonSchema);
|
|
17
|
+
|
|
18
|
+
const result = await updateLesson(db, lessonId, user.id, input);
|
|
19
|
+
if (!result) {
|
|
20
|
+
throw createError({ statusCode: 404, statusMessage: 'Lesson not found or not authorized' });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return result;
|
|
24
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createLesson } from '@commonpub/server';
|
|
2
|
+
import { createLessonSchema } from '@commonpub/schema';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const user = requireAuth(event);
|
|
6
|
+
const db = useDB();
|
|
7
|
+
const input = await parseBody(event, createLessonSchema);
|
|
8
|
+
|
|
9
|
+
return createLesson(db, user.id, input);
|
|
10
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { deleteModule } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { moduleId } = parseParams(event, { moduleId: 'uuid' });
|
|
7
|
+
|
|
8
|
+
const deleted = await deleteModule(db, moduleId, user.id);
|
|
9
|
+
if (!deleted) {
|
|
10
|
+
throw createError({ statusCode: 404, statusMessage: 'Module not found or not authorized' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return { success: true };
|
|
14
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { updateModule } from '@commonpub/server';
|
|
2
|
+
import { updateModuleSchema } from '@commonpub/schema';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const user = requireAuth(event);
|
|
6
|
+
const db = useDB();
|
|
7
|
+
const { moduleId } = parseParams(event, { moduleId: 'uuid' });
|
|
8
|
+
const input = await parseBody(event, updateModuleSchema);
|
|
9
|
+
|
|
10
|
+
const updated = await updateModule(db, moduleId, user.id, input);
|
|
11
|
+
if (!updated) {
|
|
12
|
+
throw createError({ statusCode: 404, statusMessage: 'Module not found or not authorized' });
|
|
13
|
+
}
|
|
14
|
+
return updated;
|
|
15
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createModule, getPathBySlug } from '@commonpub/server';
|
|
2
|
+
import { createModuleSchema } from '@commonpub/schema';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const user = requireAuth(event);
|
|
6
|
+
const db = useDB();
|
|
7
|
+
const { slug } = parseParams(event, { slug: 'string' });
|
|
8
|
+
const input = await parseBody(event, createModuleSchema);
|
|
9
|
+
|
|
10
|
+
const path = await getPathBySlug(db, slug, user.id);
|
|
11
|
+
if (!path) throw createError({ statusCode: 404, statusMessage: 'Path not found' });
|
|
12
|
+
|
|
13
|
+
return createModule(db, user.id, { ...input, pathId: path.id });
|
|
14
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getPathBySlug, publishPath } from '@commonpub/server';
|
|
2
|
+
import type { LearningPathDetail } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event): Promise<LearningPathDetail | null> => {
|
|
5
|
+
const user = requireAuth(event);
|
|
6
|
+
const db = useDB();
|
|
7
|
+
const { slug } = parseParams(event, { slug: 'string' });
|
|
8
|
+
|
|
9
|
+
const path = await getPathBySlug(db, slug, user.id);
|
|
10
|
+
if (!path) throw createError({ statusCode: 404, statusMessage: 'Path not found' });
|
|
11
|
+
|
|
12
|
+
return publishPath(db, path.id, user.id);
|
|
13
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getPathBySlug, unenroll } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<boolean> => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { slug } = parseParams(event, { slug: 'string' });
|
|
7
|
+
|
|
8
|
+
const path = await getPathBySlug(db, slug, user.id);
|
|
9
|
+
if (!path) throw createError({ statusCode: 404, statusMessage: 'Path not found' });
|
|
10
|
+
|
|
11
|
+
return unenroll(db, user.id, path.id);
|
|
12
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getUserCertificates } from '@commonpub/server';
|
|
2
|
+
import type { CertificateItem } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event): Promise<CertificateItem[]> => {
|
|
5
|
+
const user = requireAuth(event);
|
|
6
|
+
const db = useDB();
|
|
7
|
+
|
|
8
|
+
return getUserCertificates(db, user.id);
|
|
9
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getUserEnrollments } from '@commonpub/server';
|
|
2
|
+
import type { EnrollmentItem } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event): Promise<EnrollmentItem[]> => {
|
|
5
|
+
const user = requireAuth(event);
|
|
6
|
+
const db = useDB();
|
|
7
|
+
|
|
8
|
+
return getUserEnrollments(db, user.id);
|
|
9
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { listPaths } from '@commonpub/server';
|
|
2
|
+
import type { PaginatedResponse, LearningPathListItem } from '@commonpub/server';
|
|
3
|
+
import { learningPathFiltersSchema } from '@commonpub/schema';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event): Promise<PaginatedResponse<LearningPathListItem>> => {
|
|
6
|
+
const db = useDB();
|
|
7
|
+
const user = getOptionalUser(event);
|
|
8
|
+
const filters = parseQueryParams(event, learningPathFiltersSchema);
|
|
9
|
+
|
|
10
|
+
// Allow author to see their own drafts (same pattern as content API)
|
|
11
|
+
const isOwnContent = filters.authorId && user?.id === filters.authorId;
|
|
12
|
+
|
|
13
|
+
return listPaths(db, {
|
|
14
|
+
...filters,
|
|
15
|
+
status: isOwnContent ? filters.status : (filters.status ?? 'published'),
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createPath } from '@commonpub/server';
|
|
2
|
+
import type { LearningPathDetail } from '@commonpub/server';
|
|
3
|
+
import { createLearningPathSchema } from '@commonpub/schema';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event): Promise<LearningPathDetail> => {
|
|
6
|
+
const user = requireAuth(event);
|
|
7
|
+
const db = useDB();
|
|
8
|
+
const input = await parseBody(event, createLearningPathSchema);
|
|
9
|
+
|
|
10
|
+
return createPath(db, user.id, input);
|
|
11
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/me — Returns the current authenticated user with enriched fields (role, username, status).
|
|
3
|
+
*
|
|
4
|
+
* Unlike /api/auth/get-session which returns raw Better Auth data (missing custom columns),
|
|
5
|
+
* this endpoint reads from event.context.auth which has already been enriched by the auth middleware.
|
|
6
|
+
*/
|
|
7
|
+
export default defineEventHandler((event) => {
|
|
8
|
+
const { user, session } = event.context.auth ?? {};
|
|
9
|
+
return {
|
|
10
|
+
user: user ?? null,
|
|
11
|
+
session: session ?? null,
|
|
12
|
+
};
|
|
13
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { conversations, users } from '@commonpub/schema';
|
|
2
|
+
import { eq, and, sql, inArray } from 'drizzle-orm';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const user = requireAuth(event);
|
|
7
|
+
const { conversationId } = parseParams(event, { conversationId: 'uuid' });
|
|
8
|
+
|
|
9
|
+
const rows = await db
|
|
10
|
+
.select()
|
|
11
|
+
.from(conversations)
|
|
12
|
+
.where(
|
|
13
|
+
and(
|
|
14
|
+
eq(conversations.id, conversationId),
|
|
15
|
+
sql`${conversations.participants} @> ${JSON.stringify([user.id])}::jsonb`,
|
|
16
|
+
),
|
|
17
|
+
)
|
|
18
|
+
.limit(1);
|
|
19
|
+
|
|
20
|
+
if (!rows.length) {
|
|
21
|
+
throw createError({ statusCode: 404, statusMessage: 'Conversation not found' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const participantIds = (rows[0]!.participants ?? []) as string[];
|
|
25
|
+
|
|
26
|
+
// Resolve participant IDs to display names
|
|
27
|
+
const participantUsers = participantIds.length > 0
|
|
28
|
+
? await db
|
|
29
|
+
.select({ id: users.id, username: users.username, displayName: users.displayName, avatarUrl: users.avatarUrl })
|
|
30
|
+
.from(users)
|
|
31
|
+
.where(inArray(users.id, participantIds))
|
|
32
|
+
: [];
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
id: rows[0]!.id,
|
|
36
|
+
participants: participantUsers.map((u) => ({
|
|
37
|
+
id: u.id,
|
|
38
|
+
username: u.username,
|
|
39
|
+
displayName: u.displayName ?? u.username,
|
|
40
|
+
avatarUrl: u.avatarUrl,
|
|
41
|
+
})),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getConversationMessages } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const userId = user.id;
|
|
6
|
+
const { conversationId } = parseParams(event, { conversationId: 'uuid' });
|
|
7
|
+
|
|
8
|
+
const db = useDB();
|
|
9
|
+
|
|
10
|
+
setResponseHeader(event, 'Content-Type', 'text/event-stream');
|
|
11
|
+
setResponseHeader(event, 'Cache-Control', 'no-cache');
|
|
12
|
+
setResponseHeader(event, 'Connection', 'keep-alive');
|
|
13
|
+
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
let lastMessageCount = 0;
|
|
16
|
+
|
|
17
|
+
const stream = new ReadableStream({
|
|
18
|
+
async start(controller) {
|
|
19
|
+
// Send initial messages
|
|
20
|
+
try {
|
|
21
|
+
const msgs = await getConversationMessages(db, conversationId, userId);
|
|
22
|
+
lastMessageCount = msgs.length;
|
|
23
|
+
controller.enqueue(
|
|
24
|
+
encoder.encode(`data: ${JSON.stringify({ type: 'init', messages: msgs })}\n\n`),
|
|
25
|
+
);
|
|
26
|
+
} catch {
|
|
27
|
+
controller.close();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Poll for new messages every 3 seconds
|
|
32
|
+
const interval = setInterval(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const msgs = await getConversationMessages(db, conversationId, userId);
|
|
35
|
+
if (msgs.length > lastMessageCount) {
|
|
36
|
+
const newMsgs = msgs.slice(lastMessageCount);
|
|
37
|
+
lastMessageCount = msgs.length;
|
|
38
|
+
controller.enqueue(
|
|
39
|
+
encoder.encode(`data: ${JSON.stringify({ type: 'new', messages: newMsgs })}\n\n`),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
clearInterval(interval);
|
|
44
|
+
controller.close();
|
|
45
|
+
}
|
|
46
|
+
}, 3000);
|
|
47
|
+
|
|
48
|
+
// Keepalive every 25 seconds
|
|
49
|
+
const keepalive = setInterval(() => {
|
|
50
|
+
try {
|
|
51
|
+
controller.enqueue(encoder.encode(': keepalive\n\n'));
|
|
52
|
+
} catch {
|
|
53
|
+
clearInterval(keepalive);
|
|
54
|
+
clearInterval(interval);
|
|
55
|
+
}
|
|
56
|
+
}, 25000);
|
|
57
|
+
|
|
58
|
+
event.node.req.on('close', () => {
|
|
59
|
+
clearInterval(interval);
|
|
60
|
+
clearInterval(keepalive);
|
|
61
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return new Response(stream, {
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'text/event-stream',
|
|
69
|
+
'Cache-Control': 'no-cache',
|
|
70
|
+
'Connection': 'keep-alive',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getConversationMessages, markMessagesRead } from '@commonpub/server';
|
|
2
|
+
import type { MessageItem } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event): Promise<MessageItem[]> => {
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const user = requireAuth(event);
|
|
7
|
+
const { conversationId } = parseParams(event, { conversationId: 'uuid' });
|
|
8
|
+
|
|
9
|
+
const messages = await getConversationMessages(db, conversationId, user.id);
|
|
10
|
+
await markMessagesRead(db, conversationId, user.id);
|
|
11
|
+
|
|
12
|
+
return messages;
|
|
13
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { sendMessage } from '@commonpub/server';
|
|
2
|
+
import type { MessageItem } from '@commonpub/server';
|
|
3
|
+
import { sendMessageSchema } from '@commonpub/schema';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event): Promise<MessageItem> => {
|
|
6
|
+
const db = useDB();
|
|
7
|
+
const user = requireAuth(event);
|
|
8
|
+
const { conversationId } = parseParams(event, { conversationId: 'uuid' });
|
|
9
|
+
const input = await parseBody(event, sendMessageSchema);
|
|
10
|
+
|
|
11
|
+
return sendMessage(db, conversationId, user.id, input.body);
|
|
12
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { listConversations } from '@commonpub/server';
|
|
2
|
+
import { users } from '@commonpub/schema';
|
|
3
|
+
import { inArray } from 'drizzle-orm';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const db = useDB();
|
|
7
|
+
const user = requireAuth(event);
|
|
8
|
+
|
|
9
|
+
const conversations = await listConversations(db, user.id);
|
|
10
|
+
|
|
11
|
+
// Collect all unique participant IDs
|
|
12
|
+
const allIds = new Set<string>();
|
|
13
|
+
for (const conv of conversations) {
|
|
14
|
+
for (const id of (conv.participants ?? [])) {
|
|
15
|
+
allIds.add(id);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Resolve IDs to user profiles
|
|
20
|
+
const userMap = new Map<string, { username: string; displayName: string | null; avatarUrl: string | null }>();
|
|
21
|
+
if (allIds.size > 0) {
|
|
22
|
+
const resolved = await db
|
|
23
|
+
.select({ id: users.id, username: users.username, displayName: users.displayName, avatarUrl: users.avatarUrl })
|
|
24
|
+
.from(users)
|
|
25
|
+
.where(inArray(users.id, [...allIds]));
|
|
26
|
+
for (const u of resolved) {
|
|
27
|
+
userMap.set(u.id, { username: u.username, displayName: u.displayName, avatarUrl: u.avatarUrl });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Replace participant IDs with resolved user objects
|
|
32
|
+
return conversations.map((conv) => ({
|
|
33
|
+
...conv,
|
|
34
|
+
participants: (conv.participants ?? []).map((id: string) => {
|
|
35
|
+
const u = userMap.get(id);
|
|
36
|
+
return u ? { username: u.username, displayName: u.displayName, avatarUrl: u.avatarUrl } : { username: id, displayName: null, avatarUrl: null };
|
|
37
|
+
}),
|
|
38
|
+
}));
|
|
39
|
+
});
|