@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,65 @@
|
|
|
1
|
+
import { listContent, getUserByUsername } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
function escapeXml(str: string): string {
|
|
4
|
+
return str
|
|
5
|
+
.replace(/&/g, '&')
|
|
6
|
+
.replace(/</g, '<')
|
|
7
|
+
.replace(/>/g, '>')
|
|
8
|
+
.replace(/"/g, '"')
|
|
9
|
+
.replace(/'/g, ''');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default defineEventHandler(async (event) => {
|
|
13
|
+
const db = useDB();
|
|
14
|
+
const config = useRuntimeConfig();
|
|
15
|
+
const siteUrl = config.public.siteUrl as string;
|
|
16
|
+
const { username } = parseParams(event, { username: 'string' });
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const user = await getUserByUsername(db, username);
|
|
20
|
+
if (!user) {
|
|
21
|
+
throw createError({ statusCode: 404, statusMessage: 'User not found' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { items } = await listContent(db, {
|
|
25
|
+
status: 'published',
|
|
26
|
+
authorId: user.id,
|
|
27
|
+
sort: 'recent',
|
|
28
|
+
limit: 50,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const displayName = user.displayName ?? user.username;
|
|
32
|
+
const lastBuildDate = items.length > 0
|
|
33
|
+
? new Date(items[0].publishedAt ?? items[0].createdAt).toUTCString()
|
|
34
|
+
: new Date().toUTCString();
|
|
35
|
+
|
|
36
|
+
const rssItems = items.map((item) => {
|
|
37
|
+
const link = `${siteUrl}/${item.type}/${item.slug}`;
|
|
38
|
+
const pubDate = new Date(item.publishedAt ?? item.createdAt).toUTCString();
|
|
39
|
+
return ` <item>
|
|
40
|
+
<title>${escapeXml(item.title)}</title>
|
|
41
|
+
<link>${escapeXml(link)}</link>
|
|
42
|
+
<guid isPermaLink="true">${escapeXml(link)}</guid>
|
|
43
|
+
<pubDate>${pubDate}</pubDate>
|
|
44
|
+
<description>${escapeXml(item.description ?? '')}</description>
|
|
45
|
+
<category>${escapeXml(item.type)}</category>
|
|
46
|
+
</item>`;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
50
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
51
|
+
<channel>
|
|
52
|
+
<title>${escapeXml(displayName)} — CommonPub</title>
|
|
53
|
+
<link>${escapeXml(siteUrl)}/profile/${escapeXml(username)}</link>
|
|
54
|
+
<description>Content by ${escapeXml(displayName)}</description>
|
|
55
|
+
<language>en</language>
|
|
56
|
+
<lastBuildDate>${lastBuildDate}</lastBuildDate>
|
|
57
|
+
<atom:link href="${escapeXml(siteUrl)}/api/users/${escapeXml(username)}/feed.xml" rel="self" type="application/rss+xml"/>
|
|
58
|
+
${rssItems.join('\n')}
|
|
59
|
+
</channel>
|
|
60
|
+
</rss>`;
|
|
61
|
+
|
|
62
|
+
setResponseHeader(event, 'Content-Type', 'application/rss+xml; charset=utf-8');
|
|
63
|
+
setResponseHeader(event, 'Cache-Control', 'public, max-age=600, stale-while-revalidate=300');
|
|
64
|
+
return xml;
|
|
65
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getUserByUsername, unfollowUser } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<{ unfollowed: boolean }> => {
|
|
4
|
+
const db = useDB();
|
|
5
|
+
const user = requireAuth(event);
|
|
6
|
+
const { username } = parseParams(event, { username: 'string' });
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const target = await getUserByUsername(db, username);
|
|
10
|
+
if (!target) {
|
|
11
|
+
throw createError({ statusCode: 404, statusMessage: 'User not found' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return unfollowUser(db, user.id, target.id);
|
|
15
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getUserByUsername, followUser } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<{ followed: boolean }> => {
|
|
4
|
+
const db = useDB();
|
|
5
|
+
const user = requireAuth(event);
|
|
6
|
+
const { username } = parseParams(event, { username: 'string' });
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const target = await getUserByUsername(db, username);
|
|
10
|
+
if (!target) {
|
|
11
|
+
throw createError({ statusCode: 404, statusMessage: 'User not found' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return followUser(db, user.id, target.id);
|
|
15
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getUserByUsername, listFollowers } from '@commonpub/server';
|
|
2
|
+
import type { PaginatedResponse, FollowUserItem } from '@commonpub/server';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const paginationSchema = z.object({
|
|
6
|
+
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
7
|
+
offset: z.coerce.number().int().min(0).optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export default defineEventHandler(async (event): Promise<PaginatedResponse<FollowUserItem>> => {
|
|
11
|
+
const db = useDB();
|
|
12
|
+
const { username } = parseParams(event, { username: 'string' });
|
|
13
|
+
const query = parseQueryParams(event, paginationSchema);
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const target = await getUserByUsername(db, username);
|
|
17
|
+
if (!target) {
|
|
18
|
+
throw createError({ statusCode: 404, statusMessage: 'User not found' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return listFollowers(db, target.id, query);
|
|
22
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getUserByUsername, listFollowing } from '@commonpub/server';
|
|
2
|
+
import type { PaginatedResponse, FollowUserItem } from '@commonpub/server';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const paginationSchema = z.object({
|
|
6
|
+
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
7
|
+
offset: z.coerce.number().int().min(0).optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export default defineEventHandler(async (event): Promise<PaginatedResponse<FollowUserItem>> => {
|
|
11
|
+
const db = useDB();
|
|
12
|
+
const { username } = parseParams(event, { username: 'string' });
|
|
13
|
+
const query = parseQueryParams(event, paginationSchema);
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const target = await getUserByUsername(db, username);
|
|
17
|
+
if (!target) {
|
|
18
|
+
throw createError({ statusCode: 404, statusMessage: 'User not found' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return listFollowing(db, target.id, query);
|
|
22
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getUserByUsername, getUserEnrollments, getUserCertificates } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
const db = useDB();
|
|
5
|
+
const { username } = parseParams(event, { username: 'string' });
|
|
6
|
+
|
|
7
|
+
const profile = await getUserByUsername(db, username);
|
|
8
|
+
if (!profile) {
|
|
9
|
+
throw createError({ statusCode: 404, statusMessage: 'User not found' });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const [enrollments, certificates] = await Promise.all([
|
|
13
|
+
getUserEnrollments(db, profile.id),
|
|
14
|
+
getUserCertificates(db, profile.id),
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
return { enrollments, certificates };
|
|
18
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getUserByUsername, isFollowing } from '@commonpub/server';
|
|
2
|
+
import type { UserProfile } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event): Promise<UserProfile & { isFollowing: boolean }> => {
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { username } = parseParams(event, { username: 'string' });
|
|
7
|
+
|
|
8
|
+
const profile = await getUserByUsername(db, username);
|
|
9
|
+
if (!profile) {
|
|
10
|
+
throw createError({ statusCode: 404, statusMessage: 'User not found' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Check if current user follows this profile
|
|
14
|
+
let followStatus = false;
|
|
15
|
+
try {
|
|
16
|
+
const auth = event.context.auth;
|
|
17
|
+
if (auth?.user?.id && auth.user.id !== profile.id) {
|
|
18
|
+
followStatus = await isFollowing(db, auth.user.id, profile.id);
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
// Not authenticated — default to false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { ...profile, isFollowing: followStatus };
|
|
25
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { users, follows } from '@commonpub/schema';
|
|
2
|
+
import { sql, desc, ilike, or, and, isNull } from 'drizzle-orm';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { escapeLike } from '@commonpub/server';
|
|
5
|
+
|
|
6
|
+
const usersQuerySchema = z.object({
|
|
7
|
+
q: z.string().max(200).optional(),
|
|
8
|
+
search: z.string().max(200).optional(),
|
|
9
|
+
limit: z.coerce.number().int().positive().max(50).optional(),
|
|
10
|
+
offset: z.coerce.number().int().min(0).optional(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export default defineEventHandler(async (event) => {
|
|
14
|
+
const db = useDB();
|
|
15
|
+
const query = parseQueryParams(event, usersQuerySchema);
|
|
16
|
+
|
|
17
|
+
const limit = query.limit ?? 20;
|
|
18
|
+
const offset = query.offset ?? 0;
|
|
19
|
+
const search = query.q || query.search;
|
|
20
|
+
|
|
21
|
+
const conditions = [isNull(users.deletedAt)];
|
|
22
|
+
if (search) {
|
|
23
|
+
const term = `%${escapeLike(search)}%`;
|
|
24
|
+
conditions.push(or(ilike(users.username, term), ilike(users.displayName, term))!);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const where = and(...conditions);
|
|
28
|
+
|
|
29
|
+
const rows = await db
|
|
30
|
+
.select({
|
|
31
|
+
id: users.id,
|
|
32
|
+
username: users.username,
|
|
33
|
+
displayName: users.displayName,
|
|
34
|
+
headline: users.headline,
|
|
35
|
+
avatarUrl: users.avatarUrl,
|
|
36
|
+
createdAt: users.createdAt,
|
|
37
|
+
})
|
|
38
|
+
.from(users)
|
|
39
|
+
.where(where)
|
|
40
|
+
.orderBy(desc(users.createdAt))
|
|
41
|
+
.limit(limit)
|
|
42
|
+
.offset(offset);
|
|
43
|
+
|
|
44
|
+
// Get follower counts in bulk
|
|
45
|
+
const userIds = rows.map((r) => r.id);
|
|
46
|
+
const followerCounts: Record<string, number> = {};
|
|
47
|
+
|
|
48
|
+
if (userIds.length > 0) {
|
|
49
|
+
const counts = await db
|
|
50
|
+
.select({
|
|
51
|
+
followingId: follows.followingId,
|
|
52
|
+
count: sql<number>`count(*)::int`,
|
|
53
|
+
})
|
|
54
|
+
.from(follows)
|
|
55
|
+
.where(sql`${follows.followingId} = ANY(ARRAY[${sql.join(userIds.map((id) => sql`${id}::uuid`), sql`, `)}])`)
|
|
56
|
+
.groupBy(follows.followingId);
|
|
57
|
+
|
|
58
|
+
for (const c of counts) {
|
|
59
|
+
followerCounts[c.followingId] = c.count;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const items = rows.map((r) => ({
|
|
64
|
+
id: r.id,
|
|
65
|
+
username: r.username,
|
|
66
|
+
displayName: r.displayName,
|
|
67
|
+
headline: r.headline,
|
|
68
|
+
avatarUrl: r.avatarUrl,
|
|
69
|
+
followerCount: followerCounts[r.id] ?? 0,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const [countResult] = await db
|
|
73
|
+
.select({ count: sql<number>`count(*)::int` })
|
|
74
|
+
.from(users)
|
|
75
|
+
.where(where);
|
|
76
|
+
|
|
77
|
+
return { items, total: countResult?.count ?? items.length };
|
|
78
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getVideoById, incrementVideoViewCount } from '@commonpub/server';
|
|
2
|
+
import type { VideoDetail } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const db = useDB();
|
|
6
|
+
const { id } = parseParams(event, { id: 'uuid' });
|
|
7
|
+
const video = await getVideoById(db, id);
|
|
8
|
+
if (!video) throw createError({ statusCode: 404, statusMessage: 'Video not found' });
|
|
9
|
+
await incrementVideoViewCount(db, id);
|
|
10
|
+
return {
|
|
11
|
+
...video,
|
|
12
|
+
author: {
|
|
13
|
+
username: video.authorUsername,
|
|
14
|
+
displayName: video.authorName,
|
|
15
|
+
avatarUrl: video.authorAvatarUrl,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { deleteVideoCategory } from '@commonpub/server';
|
|
2
|
+
|
|
3
|
+
export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
|
|
4
|
+
requireAdmin(event);
|
|
5
|
+
|
|
6
|
+
const { id } = parseParams(event, { id: 'uuid' });
|
|
7
|
+
|
|
8
|
+
const db = useDB();
|
|
9
|
+
const deleted = await deleteVideoCategory(db, id);
|
|
10
|
+
if (!deleted) {
|
|
11
|
+
throw createError({ statusCode: 404, statusMessage: 'Category not found' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return { success: true };
|
|
15
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { updateVideoCategory } from '@commonpub/server';
|
|
2
|
+
import type { VideoCategoryItem } from '@commonpub/server';
|
|
3
|
+
import { createVideoCategorySchema } from '@commonpub/schema';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event): Promise<VideoCategoryItem> => {
|
|
6
|
+
requireAdmin(event);
|
|
7
|
+
const { id } = parseParams(event, { id: 'uuid' });
|
|
8
|
+
const input = await parseBody(event, createVideoCategorySchema.partial());
|
|
9
|
+
|
|
10
|
+
const db = useDB();
|
|
11
|
+
const result = await updateVideoCategory(db, id, input);
|
|
12
|
+
if (!result) {
|
|
13
|
+
throw createError({ statusCode: 404, statusMessage: 'Category not found' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return result;
|
|
17
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { listVideoCategories } from '@commonpub/server';
|
|
2
|
+
import type { VideoCategoryItem } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (_event): Promise<VideoCategoryItem[]> => {
|
|
5
|
+
const db = useDB();
|
|
6
|
+
return listVideoCategories(db);
|
|
7
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createVideoCategory } from '@commonpub/server';
|
|
2
|
+
import type { VideoCategoryItem } from '@commonpub/server';
|
|
3
|
+
import { createVideoCategorySchema } from '@commonpub/schema';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event): Promise<VideoCategoryItem> => {
|
|
6
|
+
requireAdmin(event);
|
|
7
|
+
const db = useDB();
|
|
8
|
+
const input = await parseBody(event, createVideoCategorySchema);
|
|
9
|
+
|
|
10
|
+
return createVideoCategory(db, input);
|
|
11
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { listVideos } from '@commonpub/server';
|
|
2
|
+
import type { PaginatedResponse, VideoListItem } from '@commonpub/server';
|
|
3
|
+
import { videoFiltersSchema } from '@commonpub/schema';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const db = useDB();
|
|
7
|
+
const filters = parseQueryParams(event, videoFiltersSchema);
|
|
8
|
+
const result = await listVideos(db, filters);
|
|
9
|
+
return {
|
|
10
|
+
...result,
|
|
11
|
+
items: result.items.map((v) => ({
|
|
12
|
+
...v,
|
|
13
|
+
author: {
|
|
14
|
+
username: v.authorUsername,
|
|
15
|
+
displayName: v.authorName,
|
|
16
|
+
avatarUrl: v.authorAvatarUrl,
|
|
17
|
+
},
|
|
18
|
+
})),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createVideo } from '@commonpub/server';
|
|
2
|
+
import type { VideoDetail } from '@commonpub/server';
|
|
3
|
+
import { createVideoSchema } from '@commonpub/schema';
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event): Promise<VideoDetail> => {
|
|
6
|
+
const user = requireAuth(event);
|
|
7
|
+
const db = useDB();
|
|
8
|
+
const input = await parseBody(event, createVideoSchema);
|
|
9
|
+
|
|
10
|
+
return createVideo(db, { ...input, authorId: user.id });
|
|
11
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Nitro middleware for authentication using @commonpub/auth
|
|
2
|
+
import { createAuthMiddleware, type AuthLocals } from '@commonpub/auth';
|
|
3
|
+
import { createAuth } from '@commonpub/auth';
|
|
4
|
+
import { ConsoleEmailAdapter, SmtpEmailAdapter, ResendEmailAdapter, emailTemplates } from '@commonpub/server';
|
|
5
|
+
import type { EmailAdapter } from '@commonpub/server';
|
|
6
|
+
|
|
7
|
+
let authMiddleware: ReturnType<typeof createAuthMiddleware> | null = null;
|
|
8
|
+
|
|
9
|
+
function createEmailAdapter(): EmailAdapter {
|
|
10
|
+
const runtimeConfig = useRuntimeConfig();
|
|
11
|
+
const adapter = (runtimeConfig.emailAdapter as string) || 'console';
|
|
12
|
+
|
|
13
|
+
if (adapter === 'smtp') {
|
|
14
|
+
const host = runtimeConfig.smtpHost as string;
|
|
15
|
+
const port = parseInt(runtimeConfig.smtpPort as string, 10) || 587;
|
|
16
|
+
const user = runtimeConfig.smtpUser as string;
|
|
17
|
+
const pass = runtimeConfig.smtpPass as string;
|
|
18
|
+
const from = runtimeConfig.smtpFrom as string;
|
|
19
|
+
|
|
20
|
+
if (!host || !user || !pass || !from) {
|
|
21
|
+
console.warn('[email] SMTP configured but missing credentials — falling back to console');
|
|
22
|
+
return new ConsoleEmailAdapter();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return new SmtpEmailAdapter({ host, port, user, pass, from });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (adapter === 'resend') {
|
|
29
|
+
const apiKey = runtimeConfig.resendApiKey as string;
|
|
30
|
+
const from = runtimeConfig.resendFrom as string;
|
|
31
|
+
|
|
32
|
+
if (!apiKey || !from) {
|
|
33
|
+
console.warn('[email] Resend configured but missing API key or from address — falling back to console');
|
|
34
|
+
return new ConsoleEmailAdapter();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return new ResendEmailAdapter({ apiKey, from });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new ConsoleEmailAdapter();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getAuthMiddleware(): ReturnType<typeof createAuthMiddleware> {
|
|
44
|
+
if (authMiddleware) return authMiddleware;
|
|
45
|
+
|
|
46
|
+
const config = useConfig();
|
|
47
|
+
const db = useDB();
|
|
48
|
+
const runtimeConfig = useRuntimeConfig();
|
|
49
|
+
const siteUrl = (runtimeConfig.public?.siteUrl as string) || `https://${config.instance.domain}`;
|
|
50
|
+
const siteName = config.instance.name || 'CommonPub';
|
|
51
|
+
|
|
52
|
+
const emailAdapter = createEmailAdapter();
|
|
53
|
+
|
|
54
|
+
// In dev, trust any localhost origin so port changes don't break auth
|
|
55
|
+
const trustedOrigins = process.env.NODE_ENV !== 'production'
|
|
56
|
+
? [siteUrl, 'http://localhost:3000', 'http://localhost:3001', 'http://localhost:3002', 'http://localhost:3003', 'http://localhost:3004', 'http://localhost:3005']
|
|
57
|
+
: [siteUrl];
|
|
58
|
+
|
|
59
|
+
const auth = createAuth({
|
|
60
|
+
config,
|
|
61
|
+
db: db as unknown as Parameters<typeof createAuth>[0]['db'],
|
|
62
|
+
secret: (() => {
|
|
63
|
+
const s = runtimeConfig.authSecret as string;
|
|
64
|
+
if (!s && process.env.NODE_ENV === 'production') {
|
|
65
|
+
throw new Error('AUTH_SECRET must be set in production');
|
|
66
|
+
}
|
|
67
|
+
return s || 'dev-secret-change-me';
|
|
68
|
+
})(),
|
|
69
|
+
baseURL: siteUrl,
|
|
70
|
+
trustedOrigins,
|
|
71
|
+
emailSender: {
|
|
72
|
+
async sendResetPasswordEmail(email: string, url: string, _token: string): Promise<void> {
|
|
73
|
+
const template = emailTemplates.passwordReset(siteName, url);
|
|
74
|
+
await emailAdapter.send({ ...template, to: email });
|
|
75
|
+
},
|
|
76
|
+
async sendVerificationEmail(email: string, url: string, _token: string): Promise<void> {
|
|
77
|
+
const template = emailTemplates.verification(siteName, url);
|
|
78
|
+
await emailAdapter.send({ ...template, to: email });
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
authMiddleware = createAuthMiddleware({ auth });
|
|
84
|
+
return authMiddleware;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
declare module 'h3' {
|
|
88
|
+
interface H3EventContext {
|
|
89
|
+
auth: AuthLocals;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Enrich the session user with custom DB columns (role, username, status)
|
|
95
|
+
* that Better Auth doesn't include by default.
|
|
96
|
+
*/
|
|
97
|
+
async function enrichUser(auth: AuthLocals): Promise<void> {
|
|
98
|
+
if (!auth.user?.id) return;
|
|
99
|
+
try {
|
|
100
|
+
const db = useDB();
|
|
101
|
+
const { users } = await import('@commonpub/schema');
|
|
102
|
+
const { eq } = await import('drizzle-orm');
|
|
103
|
+
const [row] = await db.select({ role: users.role, username: users.username, status: users.status })
|
|
104
|
+
.from(users).where(eq(users.id, auth.user.id)).limit(1);
|
|
105
|
+
if (row) {
|
|
106
|
+
(auth.user as unknown as Record<string, unknown>).role = row.role;
|
|
107
|
+
(auth.user as unknown as Record<string, unknown>).username = row.username;
|
|
108
|
+
(auth.user as unknown as Record<string, unknown>).status = row.status;
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Non-fatal — user just won't have role/username
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default defineEventHandler(async (event) => {
|
|
116
|
+
const pathname = getRequestURL(event).pathname;
|
|
117
|
+
|
|
118
|
+
// Skip auth for non-API routes and static assets
|
|
119
|
+
if (!pathname.startsWith('/api') && !pathname.startsWith('/_nuxt')) {
|
|
120
|
+
// Still resolve session for SSR pages
|
|
121
|
+
try {
|
|
122
|
+
const middleware = getAuthMiddleware();
|
|
123
|
+
const headers = getRequestHeaders(event);
|
|
124
|
+
const webHeaders = new Headers(headers as Record<string, string>);
|
|
125
|
+
event.context.auth = await middleware.resolveSession(webHeaders);
|
|
126
|
+
await enrichUser(event.context.auth);
|
|
127
|
+
} catch {
|
|
128
|
+
event.context.auth = { user: null, session: null };
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let middleware: ReturnType<typeof getAuthMiddleware>;
|
|
134
|
+
try {
|
|
135
|
+
middleware = getAuthMiddleware();
|
|
136
|
+
} catch {
|
|
137
|
+
// DB not connected — fail with a clear message
|
|
138
|
+
if (pathname.startsWith('/api/auth') || pathname.startsWith('/api/')) {
|
|
139
|
+
throw createError({
|
|
140
|
+
statusCode: 503,
|
|
141
|
+
statusMessage: 'Database unavailable. Check that PostgreSQL is running.',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
event.context.auth = { user: null, session: null };
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Handle auth API routes
|
|
149
|
+
if (pathname.startsWith('/api/auth')) {
|
|
150
|
+
try {
|
|
151
|
+
const response = await middleware.handleAuthRoute(
|
|
152
|
+
toWebRequest(event),
|
|
153
|
+
pathname,
|
|
154
|
+
);
|
|
155
|
+
if (response) {
|
|
156
|
+
return sendWebResponse(event, response);
|
|
157
|
+
}
|
|
158
|
+
} catch (err: unknown) {
|
|
159
|
+
console.error('[auth] Route handler error:', err instanceof Error ? err.message : err);
|
|
160
|
+
throw createError({
|
|
161
|
+
statusCode: 500,
|
|
162
|
+
statusMessage: 'Authentication service error',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Resolve session for API requests
|
|
168
|
+
try {
|
|
169
|
+
const headers = getRequestHeaders(event);
|
|
170
|
+
const webHeaders = new Headers(headers as Record<string, string>);
|
|
171
|
+
event.context.auth = await middleware.resolveSession(webHeaders);
|
|
172
|
+
await enrichUser(event.context.auth);
|
|
173
|
+
} catch (err: unknown) {
|
|
174
|
+
// DB error during session resolution — don't silently eat it for API routes
|
|
175
|
+
if (pathname.startsWith('/api/')) {
|
|
176
|
+
console.error('[auth] Session resolution failed:', err instanceof Error ? err.message : err);
|
|
177
|
+
}
|
|
178
|
+
event.context.auth = { user: null, session: null };
|
|
179
|
+
}
|
|
180
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Feature flag route gating — returns 404 for pages of disabled features.
|
|
2
|
+
// API routes handle their own gating via requireFeature() in each handler.
|
|
3
|
+
|
|
4
|
+
const ROUTE_FEATURE_MAP: Record<string, string> = {
|
|
5
|
+
'/learn': 'learning',
|
|
6
|
+
'/docs': 'docs',
|
|
7
|
+
'/videos': 'video',
|
|
8
|
+
'/admin': 'admin',
|
|
9
|
+
'/contests': 'contests',
|
|
10
|
+
'/explainer': 'explainers',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default defineEventHandler((event) => {
|
|
14
|
+
const pathname = getRequestURL(event).pathname;
|
|
15
|
+
|
|
16
|
+
// Only gate page routes, not API/assets
|
|
17
|
+
if (pathname.startsWith('/api') || pathname.startsWith('/_nuxt') || pathname.startsWith('/__nuxt') || pathname.endsWith('_payload.json')) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const [prefix, feature] of Object.entries(ROUTE_FEATURE_MAP)) {
|
|
22
|
+
if (pathname === prefix || pathname.startsWith(prefix + '/')) {
|
|
23
|
+
const config = useConfig();
|
|
24
|
+
const flags = config.features as unknown as Record<string, boolean>;
|
|
25
|
+
if (!flags[feature]) {
|
|
26
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Security middleware — rate limiting + security headers + CSP
|
|
2
|
+
import { RateLimitStore, checkRateLimit, shouldSkipRateLimit, getSecurityHeaders, buildCspHeader, buildCspDirectives } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
const store = new RateLimitStore();
|
|
5
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
6
|
+
|
|
7
|
+
export default defineEventHandler((event) => {
|
|
8
|
+
const url = getRequestURL(event);
|
|
9
|
+
const pathname = url.pathname;
|
|
10
|
+
|
|
11
|
+
// Skip rate limiting for static assets
|
|
12
|
+
if (shouldSkipRateLimit(pathname)) return;
|
|
13
|
+
|
|
14
|
+
// Skip rate limiting in development — SSR + HMR + prefetch burns through limits instantly
|
|
15
|
+
if (!isDev) {
|
|
16
|
+
const ip = getRequestHeader(event, 'x-forwarded-for')?.split(',')[0]?.trim()
|
|
17
|
+
|| getRequestHeader(event, 'x-real-ip')
|
|
18
|
+
|| 'unknown';
|
|
19
|
+
|
|
20
|
+
const userId = event.context.auth?.user?.id as string | undefined;
|
|
21
|
+
const { result, headers: rlHeaders } = checkRateLimit(store, ip, pathname, userId);
|
|
22
|
+
|
|
23
|
+
for (const [key, value] of Object.entries(rlHeaders)) {
|
|
24
|
+
setResponseHeader(event, key, value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!result.allowed) {
|
|
28
|
+
throw createError({
|
|
29
|
+
statusCode: 429,
|
|
30
|
+
statusMessage: 'Too Many Requests',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Security headers
|
|
36
|
+
const headers = getSecurityHeaders(isDev);
|
|
37
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
38
|
+
setResponseHeader(event, key, value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Content Security Policy — skip for API responses (JSON doesn't need CSP)
|
|
42
|
+
if (!pathname.startsWith('/api/')) {
|
|
43
|
+
const cspDirectives = buildCspDirectives();
|
|
44
|
+
// Nuxt SSR emits inline scripts for payload hydration — unsafe-inline is required
|
|
45
|
+
cspDirectives['script-src'] = "'self' 'unsafe-inline'" + (isDev ? " 'unsafe-eval' blob:" : '');
|
|
46
|
+
cspDirectives['style-src'] = "'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com";
|
|
47
|
+
cspDirectives['font-src'] = "'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com";
|
|
48
|
+
if (isDev) {
|
|
49
|
+
cspDirectives['connect-src'] = "'self' ws: wss:";
|
|
50
|
+
cspDirectives['worker-src'] = "'self' blob:";
|
|
51
|
+
}
|
|
52
|
+
setResponseHeader(event, 'Content-Security-Policy', buildCspHeader(cspDirectives));
|
|
53
|
+
}
|
|
54
|
+
});
|