@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,27 @@
|
|
|
1
|
+
import { sendHubFollow, getFederatedHub } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const schema = z.object({
|
|
5
|
+
federatedHubId: z.string().uuid(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean; status: string }> => {
|
|
9
|
+
requireFeature('federation');
|
|
10
|
+
requireFeature('federateHubs');
|
|
11
|
+
requireAuth(event);
|
|
12
|
+
const db = useDB();
|
|
13
|
+
const config = useConfig();
|
|
14
|
+
const { federatedHubId } = await parseBody(event, schema);
|
|
15
|
+
|
|
16
|
+
const hub = await getFederatedHub(db, federatedHubId);
|
|
17
|
+
if (!hub) {
|
|
18
|
+
throw createError({ statusCode: 404, statusMessage: 'Federated hub not found' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (hub.followStatus === 'accepted') {
|
|
22
|
+
return { success: true, status: 'accepted' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await sendHubFollow(db, hub.actorUri, config.instance.domain);
|
|
26
|
+
return { success: true, status: 'pending' };
|
|
27
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { likeFederatedHubPost, unlikeFederatedHubPost } from '@commonpub/server';
|
|
2
|
+
import { federatedHubPosts, federatedHubPostLikes, activities, remoteActors } from '@commonpub/schema';
|
|
3
|
+
import { eq, and } from 'drizzle-orm';
|
|
4
|
+
import { AP_CONTEXT, AP_PUBLIC } from '@commonpub/protocol';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
const schema = z.object({
|
|
8
|
+
federatedHubPostId: z.string().uuid(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean; liked: boolean }> => {
|
|
12
|
+
requireFeature('federation');
|
|
13
|
+
const user = requireAuth(event);
|
|
14
|
+
const db = useDB();
|
|
15
|
+
const config = useConfig();
|
|
16
|
+
const { federatedHubPostId } = await parseBody(event, schema);
|
|
17
|
+
|
|
18
|
+
// Get the post's objectUri (the remote Note's AP URI) and author
|
|
19
|
+
const [post] = await db
|
|
20
|
+
.select({
|
|
21
|
+
objectUri: federatedHubPosts.objectUri,
|
|
22
|
+
actorUri: federatedHubPosts.actorUri,
|
|
23
|
+
})
|
|
24
|
+
.from(federatedHubPosts)
|
|
25
|
+
.where(eq(federatedHubPosts.id, federatedHubPostId))
|
|
26
|
+
.limit(1);
|
|
27
|
+
|
|
28
|
+
if (!post) {
|
|
29
|
+
throw createError({ statusCode: 404, statusMessage: 'Post not found' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check if user already liked this post
|
|
33
|
+
const [existing] = await db
|
|
34
|
+
.select({ id: federatedHubPostLikes.id })
|
|
35
|
+
.from(federatedHubPostLikes)
|
|
36
|
+
.where(and(
|
|
37
|
+
eq(federatedHubPostLikes.postId, federatedHubPostId),
|
|
38
|
+
eq(federatedHubPostLikes.userId, user.id),
|
|
39
|
+
))
|
|
40
|
+
.limit(1);
|
|
41
|
+
|
|
42
|
+
const localActorUri = `https://${config.instance.domain}/users/${user.username}`;
|
|
43
|
+
|
|
44
|
+
if (existing) {
|
|
45
|
+
// Unlike: remove like record, decrement counter, send Undo(Like)
|
|
46
|
+
await db.delete(federatedHubPostLikes).where(eq(federatedHubPostLikes.id, existing.id));
|
|
47
|
+
await unlikeFederatedHubPost(db, federatedHubPostId);
|
|
48
|
+
|
|
49
|
+
// Find the original Like activity to reference in Undo
|
|
50
|
+
const [likeAct] = await db
|
|
51
|
+
.select({ id: activities.id, payload: activities.payload })
|
|
52
|
+
.from(activities)
|
|
53
|
+
.where(and(
|
|
54
|
+
eq(activities.type, 'Like'),
|
|
55
|
+
eq(activities.actorUri, localActorUri),
|
|
56
|
+
eq(activities.objectUri, post.objectUri),
|
|
57
|
+
eq(activities.direction, 'outbound'),
|
|
58
|
+
))
|
|
59
|
+
.limit(1);
|
|
60
|
+
|
|
61
|
+
const undoActivity = {
|
|
62
|
+
'@context': AP_CONTEXT,
|
|
63
|
+
type: 'Undo',
|
|
64
|
+
id: `${localActorUri}/undo/${crypto.randomUUID()}`,
|
|
65
|
+
actor: localActorUri,
|
|
66
|
+
object: likeAct?.payload ?? {
|
|
67
|
+
type: 'Like',
|
|
68
|
+
actor: localActorUri,
|
|
69
|
+
object: post.objectUri,
|
|
70
|
+
},
|
|
71
|
+
to: [post.actorUri],
|
|
72
|
+
cc: [AP_PUBLIC],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await db.insert(activities).values({
|
|
76
|
+
type: 'Undo',
|
|
77
|
+
actorUri: localActorUri,
|
|
78
|
+
objectUri: post.objectUri,
|
|
79
|
+
payload: undoActivity,
|
|
80
|
+
direction: 'outbound',
|
|
81
|
+
status: 'pending',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return { success: true, liked: false };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Like: insert like record, increment counter, send Like
|
|
88
|
+
await db.insert(federatedHubPostLikes).values({
|
|
89
|
+
postId: federatedHubPostId,
|
|
90
|
+
userId: user.id,
|
|
91
|
+
}).onConflictDoNothing();
|
|
92
|
+
|
|
93
|
+
await likeFederatedHubPost(db, federatedHubPostId);
|
|
94
|
+
|
|
95
|
+
const likeActivity = {
|
|
96
|
+
'@context': AP_CONTEXT,
|
|
97
|
+
type: 'Like',
|
|
98
|
+
id: `${localActorUri}/likes/${crypto.randomUUID()}`,
|
|
99
|
+
actor: localActorUri,
|
|
100
|
+
object: post.objectUri,
|
|
101
|
+
to: [post.actorUri],
|
|
102
|
+
cc: [AP_PUBLIC],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
await db.insert(activities).values({
|
|
106
|
+
type: 'Like',
|
|
107
|
+
actorUri: localActorUri,
|
|
108
|
+
objectUri: post.objectUri,
|
|
109
|
+
payload: likeActivity,
|
|
110
|
+
direction: 'outbound',
|
|
111
|
+
status: 'pending',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return { success: true, liked: true };
|
|
115
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { federatedHubPostLikes } from '@commonpub/schema';
|
|
2
|
+
import { eq, and, inArray } from 'drizzle-orm';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event): Promise<{ likedPostIds: string[] }> => {
|
|
6
|
+
requireFeature('federation');
|
|
7
|
+
const user = requireAuth(event);
|
|
8
|
+
const db = useDB();
|
|
9
|
+
|
|
10
|
+
const query = getQuery(event);
|
|
11
|
+
const postIds = z.string().parse(query.postIds ?? '').split(',').filter(Boolean);
|
|
12
|
+
|
|
13
|
+
if (postIds.length === 0) return { likedPostIds: [] };
|
|
14
|
+
|
|
15
|
+
const liked = await db
|
|
16
|
+
.select({ postId: federatedHubPostLikes.postId })
|
|
17
|
+
.from(federatedHubPostLikes)
|
|
18
|
+
.where(and(
|
|
19
|
+
eq(federatedHubPostLikes.userId, user.id),
|
|
20
|
+
inArray(federatedHubPostLikes.postId, postIds),
|
|
21
|
+
));
|
|
22
|
+
|
|
23
|
+
return { likedPostIds: liked.map(l => l.postId) };
|
|
24
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { sendPostToRemoteHub, getFederatedHubPost, getFederatedHub } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const replySchema = z.object({
|
|
5
|
+
federatedHubPostId: z.string().uuid(),
|
|
6
|
+
content: z.string().min(1).max(10000),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
10
|
+
requireFeature('federation');
|
|
11
|
+
const user = requireAuth(event);
|
|
12
|
+
const db = useDB();
|
|
13
|
+
const config = useConfig();
|
|
14
|
+
const { federatedHubPostId, content } = await parseBody(event, replySchema);
|
|
15
|
+
|
|
16
|
+
const post = await getFederatedHubPost(db, federatedHubPostId);
|
|
17
|
+
if (!post) {
|
|
18
|
+
throw createError({ statusCode: 404, statusMessage: 'Post not found' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const hub = await getFederatedHub(db, post.federatedHubId);
|
|
22
|
+
if (!hub) {
|
|
23
|
+
throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const success = await sendPostToRemoteHub(
|
|
27
|
+
db,
|
|
28
|
+
user.id,
|
|
29
|
+
user.username,
|
|
30
|
+
hub.actorUri,
|
|
31
|
+
content,
|
|
32
|
+
config.instance.domain,
|
|
33
|
+
'text',
|
|
34
|
+
post.objectUri,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (!success) {
|
|
38
|
+
throw createError({ statusCode: 502, statusMessage: 'Could not reach remote hub' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { success };
|
|
42
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { sendPostToRemoteHub } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const hubPostSchema = z.object({
|
|
5
|
+
federatedHubId: z.string().uuid(),
|
|
6
|
+
hubActorUri: z.string().url(),
|
|
7
|
+
content: z.string().min(1).max(10000),
|
|
8
|
+
type: z.string().optional().default('text'),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
12
|
+
requireFeature('federation');
|
|
13
|
+
const user = requireAuth(event);
|
|
14
|
+
const db = useDB();
|
|
15
|
+
const config = useConfig();
|
|
16
|
+
const body = await parseBody(event, hubPostSchema);
|
|
17
|
+
|
|
18
|
+
const success = await sendPostToRemoteHub(
|
|
19
|
+
db,
|
|
20
|
+
user.id,
|
|
21
|
+
user.username,
|
|
22
|
+
body.hubActorUri,
|
|
23
|
+
body.content,
|
|
24
|
+
config.instance.domain,
|
|
25
|
+
body.type,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (!success) {
|
|
29
|
+
throw createError({ statusCode: 502, statusMessage: 'Could not reach remote hub' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { success };
|
|
33
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { likeRemoteContent } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const likeSchema = z.object({
|
|
5
|
+
federatedContentId: z.string().uuid(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
9
|
+
requireFeature('federation');
|
|
10
|
+
const user = requireAuth(event);
|
|
11
|
+
const db = useDB();
|
|
12
|
+
const config = useConfig();
|
|
13
|
+
const { federatedContentId } = await parseBody(event, likeSchema);
|
|
14
|
+
|
|
15
|
+
const success = await likeRemoteContent(db, user.id, federatedContentId, config.instance.domain);
|
|
16
|
+
if (!success) {
|
|
17
|
+
throw createError({ statusCode: 404, statusMessage: 'Content not found' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { success };
|
|
21
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getRemoteActorProfile } from '@commonpub/server';
|
|
2
|
+
import type { RemoteActorProfile } from '@commonpub/server';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const querySchema = z.object({
|
|
6
|
+
uri: z.string().url(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default defineEventHandler(async (event): Promise<RemoteActorProfile> => {
|
|
10
|
+
requireFeature('federation');
|
|
11
|
+
const user = requireAuth(event);
|
|
12
|
+
const db = useDB();
|
|
13
|
+
const config = useConfig();
|
|
14
|
+
const { uri } = parseQueryParams(event, querySchema);
|
|
15
|
+
|
|
16
|
+
const profile = await getRemoteActorProfile(db, uri, config.instance.domain, user.id);
|
|
17
|
+
if (!profile) {
|
|
18
|
+
throw createError({ statusCode: 404, statusMessage: 'Remote actor not found' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return profile;
|
|
22
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { federateReply } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const replySchema = z.object({
|
|
5
|
+
federatedContentId: z.string().uuid(),
|
|
6
|
+
content: z.string().min(1).max(10000),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
10
|
+
requireFeature('federation');
|
|
11
|
+
const user = requireAuth(event);
|
|
12
|
+
const db = useDB();
|
|
13
|
+
const config = useConfig();
|
|
14
|
+
const { federatedContentId, content } = await parseBody(event, replySchema);
|
|
15
|
+
|
|
16
|
+
const success = await federateReply(db, user.id, federatedContentId, content, config.instance.domain);
|
|
17
|
+
if (!success) {
|
|
18
|
+
throw createError({ statusCode: 404, statusMessage: 'Content not found' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { success };
|
|
22
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { searchRemoteActor } from '@commonpub/server';
|
|
2
|
+
import type { RemoteActorProfile } from '@commonpub/server';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const searchSchema = z.object({
|
|
6
|
+
query: z.string().min(3).max(256),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default defineEventHandler(async (event): Promise<RemoteActorProfile | null> => {
|
|
10
|
+
requireFeature('federation');
|
|
11
|
+
const user = requireAuth(event);
|
|
12
|
+
const db = useDB();
|
|
13
|
+
const config = useConfig();
|
|
14
|
+
const { query } = await parseBody(event, searchSchema);
|
|
15
|
+
|
|
16
|
+
return searchRemoteActor(db, query, config.instance.domain, user.id);
|
|
17
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { listFederatedTimeline } from '@commonpub/server';
|
|
2
|
+
import type { FederatedContentItem } from '@commonpub/server';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const querySchema = z.object({
|
|
6
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
7
|
+
offset: z.coerce.number().int().min(0).default(0),
|
|
8
|
+
apType: z.string().optional(),
|
|
9
|
+
cpubType: z.string().optional(),
|
|
10
|
+
originDomain: z.string().optional(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export default defineEventHandler(
|
|
14
|
+
async (event): Promise<{ items: FederatedContentItem[]; total: number }> => {
|
|
15
|
+
requireFeature('federation');
|
|
16
|
+
requireAuth(event);
|
|
17
|
+
const db = useDB();
|
|
18
|
+
const opts = parseQueryParams(event, querySchema);
|
|
19
|
+
|
|
20
|
+
return listFederatedTimeline(db, opts);
|
|
21
|
+
},
|
|
22
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { unfollowRemote } from '@commonpub/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const unfollowSchema = z.object({
|
|
5
|
+
actorUri: z.string().url(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
9
|
+
requireFeature('federation');
|
|
10
|
+
const user = requireAuth(event);
|
|
11
|
+
const db = useDB();
|
|
12
|
+
const config = useConfig();
|
|
13
|
+
const { actorUri } = await parseBody(event, unfollowSchema);
|
|
14
|
+
|
|
15
|
+
await unfollowRemote(db, user.id, actorUri, config.instance.domain);
|
|
16
|
+
return { success: true };
|
|
17
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { eq, and } from 'drizzle-orm';
|
|
2
|
+
import { files } from '@commonpub/schema';
|
|
3
|
+
import { createStorageFromEnv } from '@commonpub/server';
|
|
4
|
+
|
|
5
|
+
let storage: ReturnType<typeof createStorageFromEnv> | null = null;
|
|
6
|
+
function getStorage(): ReturnType<typeof createStorageFromEnv> {
|
|
7
|
+
if (!storage) storage = createStorageFromEnv();
|
|
8
|
+
return storage;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default defineEventHandler(async (event): Promise<{ deleted: boolean }> => {
|
|
12
|
+
const db = useDB();
|
|
13
|
+
const user = requireAuth(event);
|
|
14
|
+
const { id } = parseParams(event, { id: 'uuid' });
|
|
15
|
+
|
|
16
|
+
const result = await db
|
|
17
|
+
.delete(files)
|
|
18
|
+
.where(and(eq(files.id, id), eq(files.uploaderId, user.id)))
|
|
19
|
+
.returning({ id: files.id, storageKey: files.storageKey });
|
|
20
|
+
|
|
21
|
+
if (result.length === 0) {
|
|
22
|
+
throw createError({ statusCode: 404, statusMessage: 'File not found or not owned by you' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Delete from storage (best-effort, don't fail the request if storage delete fails)
|
|
26
|
+
try {
|
|
27
|
+
const adapter = getStorage();
|
|
28
|
+
await adapter.delete(result[0]!.storageKey);
|
|
29
|
+
} catch {
|
|
30
|
+
// Log but don't fail — the DB record is already deleted
|
|
31
|
+
console.warn(`[files] Failed to delete storage key: ${result[0]!.storageKey}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { deleted: true };
|
|
35
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { eq, desc } from 'drizzle-orm';
|
|
2
|
+
import { files } from '@commonpub/schema';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const querySchema = z.object({
|
|
6
|
+
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default defineEventHandler(async (event) => {
|
|
10
|
+
const db = useDB();
|
|
11
|
+
const user = requireAuth(event);
|
|
12
|
+
const query = parseQueryParams(event, querySchema);
|
|
13
|
+
|
|
14
|
+
const rows = await db
|
|
15
|
+
.select()
|
|
16
|
+
.from(files)
|
|
17
|
+
.where(eq(files.uploaderId, user.id))
|
|
18
|
+
.orderBy(desc(files.createdAt))
|
|
19
|
+
.limit(query.limit ?? 50);
|
|
20
|
+
|
|
21
|
+
return rows.map((f) => ({
|
|
22
|
+
id: f.id,
|
|
23
|
+
filename: f.filename,
|
|
24
|
+
originalName: f.originalName,
|
|
25
|
+
mimeType: f.mimeType,
|
|
26
|
+
sizeBytes: f.sizeBytes,
|
|
27
|
+
url: f.publicUrl,
|
|
28
|
+
purpose: f.purpose,
|
|
29
|
+
createdAt: f.createdAt,
|
|
30
|
+
}));
|
|
31
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createStorageFromEnv, generateStorageKey, validateUpload, ALLOWED_IMAGE_TYPES } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
const schema = z.object({
|
|
5
|
+
url: z.string().url(),
|
|
6
|
+
purpose: z.enum(['content', 'cover', 'avatar', 'banner']).default('content'),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default defineEventHandler(async (event) => {
|
|
10
|
+
const user = requireAuth(event);
|
|
11
|
+
const { url, purpose } = await parseBody(event, schema);
|
|
12
|
+
|
|
13
|
+
// SSRF protection — block private IPs
|
|
14
|
+
const parsed = new URL(url);
|
|
15
|
+
const hostname = parsed.hostname;
|
|
16
|
+
if (
|
|
17
|
+
hostname === 'localhost' ||
|
|
18
|
+
hostname === '127.0.0.1' ||
|
|
19
|
+
hostname === '::1' ||
|
|
20
|
+
hostname.startsWith('10.') ||
|
|
21
|
+
hostname.startsWith('192.168.') ||
|
|
22
|
+
hostname.match(/^172\.(1[6-9]|2\d|3[01])\./) ||
|
|
23
|
+
hostname === '169.254.169.254' // AWS metadata
|
|
24
|
+
) {
|
|
25
|
+
throw createError({ statusCode: 400, statusMessage: 'Cannot fetch from private/local addresses' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Download the remote image
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timeout = setTimeout(() => controller.abort(), 10_000); // 10s timeout
|
|
31
|
+
|
|
32
|
+
let response: Response;
|
|
33
|
+
try {
|
|
34
|
+
response = await fetch(url, {
|
|
35
|
+
signal: controller.signal,
|
|
36
|
+
headers: { 'User-Agent': 'devEco.io Image Fetcher' },
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw createError({ statusCode: 400, statusMessage: 'Failed to fetch remote image' });
|
|
40
|
+
} finally {
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw createError({ statusCode: 400, statusMessage: `Remote server returned ${response.status}` });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const contentType = response.headers.get('content-type') || '';
|
|
49
|
+
if (![...ALLOWED_IMAGE_TYPES].some((t: string) => contentType.startsWith(t))) {
|
|
50
|
+
throw createError({ statusCode: 400, statusMessage: `Unsupported image type: ${contentType}` });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
54
|
+
const maxSize = 10 * 1024 * 1024; // 10MB
|
|
55
|
+
if (buffer.length > maxSize) {
|
|
56
|
+
throw createError({ statusCode: 400, statusMessage: 'Image too large (max 10MB)' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Upload to storage
|
|
60
|
+
const storage = createStorageFromEnv();
|
|
61
|
+
const ext = contentType.split('/')[1] || 'jpg';
|
|
62
|
+
const key = generateStorageKey(purpose, ext);
|
|
63
|
+
|
|
64
|
+
await (storage as any).put(key, buffer, contentType);
|
|
65
|
+
const resultUrl = (storage as any).getPublicUrl(key);
|
|
66
|
+
|
|
67
|
+
return { url: resultUrl };
|
|
68
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File upload endpoint.
|
|
3
|
+
* Accepts multipart form data, validates file type/size, stores via configured adapter.
|
|
4
|
+
* Images are processed into WebP variants (thumb/small/medium/large).
|
|
5
|
+
*/
|
|
6
|
+
import { files } from '@commonpub/schema';
|
|
7
|
+
import {
|
|
8
|
+
createStorageFromEnv,
|
|
9
|
+
generateStorageKey,
|
|
10
|
+
validateUpload,
|
|
11
|
+
isProcessableImage,
|
|
12
|
+
processImage,
|
|
13
|
+
} from '@commonpub/server';
|
|
14
|
+
|
|
15
|
+
// Lazy-init storage adapter (created once on first request)
|
|
16
|
+
let storage: ReturnType<typeof createStorageFromEnv> | null = null;
|
|
17
|
+
function getStorage(): ReturnType<typeof createStorageFromEnv> {
|
|
18
|
+
if (!storage) storage = createStorageFromEnv();
|
|
19
|
+
return storage;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default defineEventHandler(async (event) => {
|
|
23
|
+
const db = useDB();
|
|
24
|
+
const user = requireAuth(event);
|
|
25
|
+
|
|
26
|
+
const formData = await readMultipartFormData(event);
|
|
27
|
+
if (!formData || formData.length === 0) {
|
|
28
|
+
throw createError({ statusCode: 400, statusMessage: 'No file uploaded' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const file = formData[0]!;
|
|
32
|
+
const filename = file.filename || `upload-${Date.now()}`;
|
|
33
|
+
const mimeType = file.type || 'application/octet-stream';
|
|
34
|
+
const sizeBytes = file.data.length;
|
|
35
|
+
const validPurposes = ['cover', 'content', 'avatar', 'banner', 'attachment'] as const;
|
|
36
|
+
type Purpose = typeof validPurposes[number];
|
|
37
|
+
const purposeRaw = formData.find((f) => f.name === 'purpose')?.data.toString() || 'content';
|
|
38
|
+
if (!validPurposes.includes(purposeRaw as Purpose)) {
|
|
39
|
+
throw createError({ statusCode: 400, statusMessage: 'Invalid upload purpose' });
|
|
40
|
+
}
|
|
41
|
+
const purpose = purposeRaw as Purpose;
|
|
42
|
+
|
|
43
|
+
// Validate
|
|
44
|
+
const validation = validateUpload(mimeType, sizeBytes, purpose);
|
|
45
|
+
if (!validation.valid) {
|
|
46
|
+
throw createError({ statusCode: 400, statusMessage: validation.error ?? 'Invalid upload' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const adapter = getStorage();
|
|
50
|
+
let publicUrl: string;
|
|
51
|
+
let storageKey: string;
|
|
52
|
+
let width: number | null = null;
|
|
53
|
+
let height: number | null = null;
|
|
54
|
+
let variants: Record<string, string> | null = null;
|
|
55
|
+
|
|
56
|
+
if (isProcessableImage(mimeType)) {
|
|
57
|
+
// Process image: generate thumbnails and convert to WebP
|
|
58
|
+
const processed = await processImage(file.data, filename, purpose, adapter, mimeType);
|
|
59
|
+
publicUrl = processed.originalUrl;
|
|
60
|
+
storageKey = processed.originalKey;
|
|
61
|
+
width = processed.width;
|
|
62
|
+
height = processed.height;
|
|
63
|
+
|
|
64
|
+
if (processed.variants.length > 0) {
|
|
65
|
+
variants = {};
|
|
66
|
+
for (const v of processed.variants) {
|
|
67
|
+
variants[v.name] = v.url;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Non-image file: upload as-is
|
|
72
|
+
storageKey = generateStorageKey(filename, purpose);
|
|
73
|
+
publicUrl = await adapter.upload(storageKey, file.data, mimeType);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Store metadata in DB
|
|
77
|
+
const [row] = await db
|
|
78
|
+
.insert(files)
|
|
79
|
+
.values({
|
|
80
|
+
uploaderId: user.id,
|
|
81
|
+
filename: storageKey,
|
|
82
|
+
originalName: filename,
|
|
83
|
+
mimeType,
|
|
84
|
+
sizeBytes,
|
|
85
|
+
storageKey,
|
|
86
|
+
publicUrl,
|
|
87
|
+
purpose,
|
|
88
|
+
width,
|
|
89
|
+
height,
|
|
90
|
+
})
|
|
91
|
+
.returning();
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
id: row!.id,
|
|
95
|
+
filename: row!.filename,
|
|
96
|
+
originalName: filename,
|
|
97
|
+
mimeType: row!.mimeType,
|
|
98
|
+
sizeBytes: row!.sizeBytes,
|
|
99
|
+
url: publicUrl,
|
|
100
|
+
width,
|
|
101
|
+
height,
|
|
102
|
+
variants,
|
|
103
|
+
purpose: row!.purpose,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { unbanUser, getHubBySlug } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<{ unbanned: boolean; error?: string }> => {
|
|
4
|
+
const user = requireAuth(event);
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { slug, userId } = parseParams(event, { slug: 'string', userId: 'uuid' });
|
|
7
|
+
const community = await getHubBySlug(db, slug);
|
|
8
|
+
if (!community) {
|
|
9
|
+
throw createError({ statusCode: 404, statusMessage: 'Community not found' });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return unbanUser(db, user.id, community.id, userId);
|
|
13
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { listBans, getHubBySlug, getMember } from '@commonpub/server';
|
|
2
|
+
import type { HubBanItem } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event): Promise<HubBanItem[]> => {
|
|
5
|
+
const user = requireAuth(event);
|
|
6
|
+
const db = useDB();
|
|
7
|
+
const { slug } = parseParams(event, { slug: 'string' });
|
|
8
|
+
const hub = await getHubBySlug(db, slug);
|
|
9
|
+
if (!hub) {
|
|
10
|
+
throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Only moderators, admins, and owners can view ban lists
|
|
14
|
+
const member = await getMember(db, hub.id, user.id);
|
|
15
|
+
if (!member || !['moderator', 'admin', 'owner'].includes(member.role)) {
|
|
16
|
+
throw createError({ statusCode: 403, statusMessage: 'Insufficient permissions' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return listBans(db, hub.id);
|
|
20
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { banUser, getHubBySlug } from '@commonpub/server';
|
|
2
|
+
import { banUserSchema } from '@commonpub/schema';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event): Promise<{ banned: boolean; error?: string }> => {
|
|
5
|
+
const user = requireAuth(event);
|
|
6
|
+
const db = useDB();
|
|
7
|
+
const { slug } = parseParams(event, { slug: 'string' });
|
|
8
|
+
const input = await parseBody(event, banUserSchema);
|
|
9
|
+
|
|
10
|
+
const hub = await getHubBySlug(db, slug);
|
|
11
|
+
if (!hub) {
|
|
12
|
+
throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return banUser(
|
|
16
|
+
db,
|
|
17
|
+
user.id,
|
|
18
|
+
hub.id,
|
|
19
|
+
input.userId,
|
|
20
|
+
input.reason,
|
|
21
|
+
input.expiresAt ? new Date(input.expiresAt) : undefined,
|
|
22
|
+
);
|
|
23
|
+
});
|