@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.
Files changed (436) hide show
  1. package/LICENSE +661 -0
  2. package/app.vue +7 -0
  3. package/components/AnnouncementBand.vue +117 -0
  4. package/components/AppToast.vue +108 -0
  5. package/components/AuthorCard.vue +119 -0
  6. package/components/AuthorRow.vue +81 -0
  7. package/components/CommentSection.vue +330 -0
  8. package/components/ContentCard.vue +340 -0
  9. package/components/ContentPicker.vue +240 -0
  10. package/components/ContentStarterForm.vue +214 -0
  11. package/components/ContentTypeBadge.vue +46 -0
  12. package/components/CountdownTimer.vue +68 -0
  13. package/components/CpubEditor.vue +87 -0
  14. package/components/DiscussionItem.vue +191 -0
  15. package/components/EditorPropertiesPanel.vue +393 -0
  16. package/components/EngagementBar.vue +131 -0
  17. package/components/FederatedContentCard.vue +291 -0
  18. package/components/FeedItem.vue +283 -0
  19. package/components/FilterChip.vue +21 -0
  20. package/components/HeatmapGrid.vue +92 -0
  21. package/components/ImageUpload.vue +219 -0
  22. package/components/MemberCard.vue +163 -0
  23. package/components/MessageThread.vue +120 -0
  24. package/components/NotificationItem.vue +103 -0
  25. package/components/ProgressTracker.vue +41 -0
  26. package/components/PublishErrorsModal.vue +116 -0
  27. package/components/RemoteActorCard.vue +206 -0
  28. package/components/RemoteUserSearch.vue +117 -0
  29. package/components/SearchFilters.vue +188 -0
  30. package/components/SearchSidebar.vue +181 -0
  31. package/components/SectionHeader.vue +17 -0
  32. package/components/ShareToHubModal.vue +189 -0
  33. package/components/SiteLogo.vue +21 -0
  34. package/components/SkillBar.vue +57 -0
  35. package/components/SortSelect.vue +30 -0
  36. package/components/StatBar.vue +14 -0
  37. package/components/TOCNav.vue +69 -0
  38. package/components/TimelineItem.vue +82 -0
  39. package/components/VideoCard.vue +106 -0
  40. package/components/blocks/BlockBuildStepView.vue +92 -0
  41. package/components/blocks/BlockCalloutView.vue +82 -0
  42. package/components/blocks/BlockCheckpointView.vue +50 -0
  43. package/components/blocks/BlockCodeView.vue +212 -0
  44. package/components/blocks/BlockContentRenderer.vue +143 -0
  45. package/components/blocks/BlockDividerView.vue +11 -0
  46. package/components/blocks/BlockDownloadsView.vue +126 -0
  47. package/components/blocks/BlockEmbedView.vue +61 -0
  48. package/components/blocks/BlockGalleryView.vue +57 -0
  49. package/components/blocks/BlockHeadingView.vue +29 -0
  50. package/components/blocks/BlockImageView.vue +34 -0
  51. package/components/blocks/BlockMarkdownView.vue +118 -0
  52. package/components/blocks/BlockMathView.vue +45 -0
  53. package/components/blocks/BlockPartsListView.vue +104 -0
  54. package/components/blocks/BlockQuizView.vue +239 -0
  55. package/components/blocks/BlockQuoteView.vue +41 -0
  56. package/components/blocks/BlockSectionHeaderView.vue +58 -0
  57. package/components/blocks/BlockSliderView.vue +236 -0
  58. package/components/blocks/BlockTextView.vue +41 -0
  59. package/components/blocks/BlockToolListView.vue +87 -0
  60. package/components/blocks/BlockVideoView.vue +89 -0
  61. package/components/editors/ArticleEditor.vue +545 -0
  62. package/components/editors/BlockCanvas.vue +487 -0
  63. package/components/editors/BlockInsertZone.vue +84 -0
  64. package/components/editors/BlockPicker.vue +285 -0
  65. package/components/editors/BlockWrapper.vue +192 -0
  66. package/components/editors/BlogEditor.vue +567 -0
  67. package/components/editors/EditorBlocks.vue +248 -0
  68. package/components/editors/EditorSection.vue +81 -0
  69. package/components/editors/EditorShell.vue +168 -0
  70. package/components/editors/EditorTagInput.vue +114 -0
  71. package/components/editors/EditorVisibility.vue +110 -0
  72. package/components/editors/ExplainerEditor.vue +503 -0
  73. package/components/editors/MarkdownImportDialog.vue +249 -0
  74. package/components/editors/ProjectEditor.vue +446 -0
  75. package/components/editors/blocks/BuildStepBlock.vue +102 -0
  76. package/components/editors/blocks/CalloutBlock.vue +122 -0
  77. package/components/editors/blocks/CheckpointBlock.vue +27 -0
  78. package/components/editors/blocks/CodeBlock.vue +177 -0
  79. package/components/editors/blocks/DividerBlock.vue +22 -0
  80. package/components/editors/blocks/DownloadsBlock.vue +41 -0
  81. package/components/editors/blocks/EmbedBlock.vue +20 -0
  82. package/components/editors/blocks/GalleryBlock.vue +236 -0
  83. package/components/editors/blocks/HeadingBlock.vue +96 -0
  84. package/components/editors/blocks/ImageBlock.vue +271 -0
  85. package/components/editors/blocks/MarkdownBlock.vue +258 -0
  86. package/components/editors/blocks/MathBlock.vue +37 -0
  87. package/components/editors/blocks/PartsListBlock.vue +358 -0
  88. package/components/editors/blocks/QuizBlock.vue +47 -0
  89. package/components/editors/blocks/QuoteBlock.vue +101 -0
  90. package/components/editors/blocks/SectionHeaderBlock.vue +130 -0
  91. package/components/editors/blocks/SliderBlock.vue +318 -0
  92. package/components/editors/blocks/TextBlock.vue +201 -0
  93. package/components/editors/blocks/ToolListBlock.vue +70 -0
  94. package/components/editors/blocks/VideoBlock.vue +22 -0
  95. package/components/hub/HubDiscussions.vue +47 -0
  96. package/components/hub/HubFeed.vue +199 -0
  97. package/components/hub/HubHero.vue +185 -0
  98. package/components/hub/HubLayout.vue +103 -0
  99. package/components/hub/HubMembers.vue +40 -0
  100. package/components/hub/HubProducts.vue +93 -0
  101. package/components/hub/HubProjects.vue +207 -0
  102. package/components/hub/HubSidebar.vue +12 -0
  103. package/components/hub/HubSidebarCard.vue +35 -0
  104. package/components/views/ArticleView.vue +771 -0
  105. package/components/views/BlogView.vue +667 -0
  106. package/components/views/ExplainerView.vue +688 -0
  107. package/components/views/ProjectView.vue +1500 -0
  108. package/composables/useApiError.ts +39 -0
  109. package/composables/useAuth.ts +87 -0
  110. package/composables/useBlockEditor.ts +187 -0
  111. package/composables/useContentSave.ts +253 -0
  112. package/composables/useContentTypes.ts +37 -0
  113. package/composables/useEngagement.ts +196 -0
  114. package/composables/useFeatures.ts +33 -0
  115. package/composables/useFederation.ts +72 -0
  116. package/composables/useJsonLd.ts +183 -0
  117. package/composables/useMarkdownImport.ts +77 -0
  118. package/composables/useMirrorContent.ts +105 -0
  119. package/composables/useNotifications.ts +73 -0
  120. package/composables/usePublishValidation.ts +65 -0
  121. package/composables/useSanitize.ts +34 -0
  122. package/composables/useSiteName.ts +4 -0
  123. package/composables/useTheme.ts +34 -0
  124. package/composables/useToast.ts +35 -0
  125. package/error.vue +129 -0
  126. package/layouts/admin.vue +213 -0
  127. package/layouts/auth.vue +63 -0
  128. package/layouts/default.vue +269 -0
  129. package/layouts/editor.vue +129 -0
  130. package/middleware/auth.ts +6 -0
  131. package/nuxt.config.ts +83 -0
  132. package/package.json +59 -0
  133. package/pages/[type]/[slug]/edit.vue +676 -0
  134. package/pages/[type]/[slug]/index.vue +313 -0
  135. package/pages/[type]/index.vue +118 -0
  136. package/pages/about.vue +100 -0
  137. package/pages/admin/audit.vue +66 -0
  138. package/pages/admin/content.vue +116 -0
  139. package/pages/admin/federation.vue +446 -0
  140. package/pages/admin/index.vue +62 -0
  141. package/pages/admin/reports.vue +88 -0
  142. package/pages/admin/settings.vue +167 -0
  143. package/pages/admin/users.vue +145 -0
  144. package/pages/auth/forgot-password.vue +103 -0
  145. package/pages/auth/login.vue +216 -0
  146. package/pages/auth/oauth/authorize.vue +178 -0
  147. package/pages/auth/register.vue +246 -0
  148. package/pages/auth/reset-password.vue +124 -0
  149. package/pages/auth/verify-email.vue +80 -0
  150. package/pages/cert/[code].vue +154 -0
  151. package/pages/contests/[slug]/edit.vue +153 -0
  152. package/pages/contests/[slug]/index.vue +556 -0
  153. package/pages/contests/[slug]/judge.vue +160 -0
  154. package/pages/contests/create.vue +192 -0
  155. package/pages/contests/index.vue +69 -0
  156. package/pages/create.vue +219 -0
  157. package/pages/dashboard.vue +521 -0
  158. package/pages/docs/[siteSlug]/[...pagePath].vue +704 -0
  159. package/pages/docs/[siteSlug]/edit.vue +480 -0
  160. package/pages/docs/[siteSlug]/index.vue +380 -0
  161. package/pages/docs/create.vue +60 -0
  162. package/pages/docs/index.vue +181 -0
  163. package/pages/explore.vue +422 -0
  164. package/pages/federated-hubs/[id]/index.vue +385 -0
  165. package/pages/federated-hubs/[id]/posts/[postId].vue +309 -0
  166. package/pages/federation/index.vue +157 -0
  167. package/pages/federation/search.vue +43 -0
  168. package/pages/federation/users/[handle].vue +221 -0
  169. package/pages/feed.vue +135 -0
  170. package/pages/hubs/[slug]/index.vue +513 -0
  171. package/pages/hubs/[slug]/members.vue +134 -0
  172. package/pages/hubs/[slug]/posts/[postId].vue +352 -0
  173. package/pages/hubs/[slug]/settings.vue +254 -0
  174. package/pages/hubs/create.vue +201 -0
  175. package/pages/hubs/index.vue +207 -0
  176. package/pages/index.vue +1005 -0
  177. package/pages/learn/[slug]/[lessonSlug]/edit.vue +413 -0
  178. package/pages/learn/[slug]/[lessonSlug]/index.vue +438 -0
  179. package/pages/learn/[slug]/edit.vue +414 -0
  180. package/pages/learn/[slug]/index.vue +341 -0
  181. package/pages/learn/create.vue +71 -0
  182. package/pages/learn/index.vue +360 -0
  183. package/pages/messages/[conversationId].vue +113 -0
  184. package/pages/messages/index.vue +303 -0
  185. package/pages/mirror/[id].vue +115 -0
  186. package/pages/notifications.vue +91 -0
  187. package/pages/products/[slug].vue +128 -0
  188. package/pages/products/index.vue +122 -0
  189. package/pages/search.vue +692 -0
  190. package/pages/settings/account.vue +170 -0
  191. package/pages/settings/appearance.vue +80 -0
  192. package/pages/settings/index.vue +81 -0
  193. package/pages/settings/notifications.vue +68 -0
  194. package/pages/settings/profile.vue +838 -0
  195. package/pages/tags/[slug].vue +111 -0
  196. package/pages/tags/index.vue +73 -0
  197. package/pages/u/[username]/followers.vue +86 -0
  198. package/pages/u/[username]/following.vue +94 -0
  199. package/pages/u/[username]/index.vue +837 -0
  200. package/pages/videos/[id].vue +212 -0
  201. package/pages/videos/index.vue +327 -0
  202. package/pages/videos/submit.vue +112 -0
  203. package/plugins/auth.ts +23 -0
  204. package/server/api/admin/audit.get.ts +17 -0
  205. package/server/api/admin/content/[id].delete.ts +15 -0
  206. package/server/api/admin/content/[id].patch.ts +37 -0
  207. package/server/api/admin/federation/activity.get.ts +31 -0
  208. package/server/api/admin/federation/clients.get.ts +9 -0
  209. package/server/api/admin/federation/clients.post.ts +16 -0
  210. package/server/api/admin/federation/hub-mirrors/index.get.ts +10 -0
  211. package/server/api/admin/federation/hub-mirrors/index.post.ts +42 -0
  212. package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +39 -0
  213. package/server/api/admin/federation/mirrors/[id].delete.ts +11 -0
  214. package/server/api/admin/federation/mirrors/[id].get.ts +15 -0
  215. package/server/api/admin/federation/mirrors/[id].put.ts +22 -0
  216. package/server/api/admin/federation/mirrors/index.get.ts +9 -0
  217. package/server/api/admin/federation/mirrors/index.post.ts +30 -0
  218. package/server/api/admin/federation/pending.get.ts +22 -0
  219. package/server/api/admin/federation/refederate.post.ts +92 -0
  220. package/server/api/admin/federation/repair-types.post.ts +57 -0
  221. package/server/api/admin/federation/retry.post.ts +41 -0
  222. package/server/api/admin/federation/stats.get.ts +26 -0
  223. package/server/api/admin/reports/[id]/resolve.post.ts +12 -0
  224. package/server/api/admin/reports.get.ts +17 -0
  225. package/server/api/admin/settings.get.ts +22 -0
  226. package/server/api/admin/settings.put.ts +11 -0
  227. package/server/api/admin/stats.get.ts +9 -0
  228. package/server/api/admin/users/[id]/role.put.ts +12 -0
  229. package/server/api/admin/users/[id]/status.put.ts +12 -0
  230. package/server/api/admin/users/[id].delete.ts +10 -0
  231. package/server/api/admin/users.get.ts +18 -0
  232. package/server/api/auth/federated/callback.get.ts +67 -0
  233. package/server/api/auth/federated/login.post.ts +60 -0
  234. package/server/api/auth/oauth2/authorize.get.ts +30 -0
  235. package/server/api/auth/oauth2/authorize.post.ts +51 -0
  236. package/server/api/auth/oauth2/register.post.ts +41 -0
  237. package/server/api/auth/oauth2/token.post.ts +48 -0
  238. package/server/api/cert/[code].get.ts +13 -0
  239. package/server/api/content/[id]/build.post.ts +9 -0
  240. package/server/api/content/[id]/fork.post.ts +10 -0
  241. package/server/api/content/[id]/index.delete.ts +18 -0
  242. package/server/api/content/[id]/index.get.ts +15 -0
  243. package/server/api/content/[id]/index.put.ts +23 -0
  244. package/server/api/content/[id]/products/[productId].delete.ts +28 -0
  245. package/server/api/content/[id]/products-sync.post.ts +29 -0
  246. package/server/api/content/[id]/products.get.ts +9 -0
  247. package/server/api/content/[id]/products.post.ts +31 -0
  248. package/server/api/content/[id]/publish.post.ts +17 -0
  249. package/server/api/content/[id]/report.post.ts +17 -0
  250. package/server/api/content/[id]/versions.get.ts +9 -0
  251. package/server/api/content/[id]/view.post.ts +34 -0
  252. package/server/api/content/index.get.ts +23 -0
  253. package/server/api/content/index.post.ts +11 -0
  254. package/server/api/contests/[slug]/entries.get.ts +18 -0
  255. package/server/api/contests/[slug]/entries.post.ts +23 -0
  256. package/server/api/contests/[slug]/index.delete.ts +21 -0
  257. package/server/api/contests/[slug]/index.get.ts +11 -0
  258. package/server/api/contests/[slug]/index.put.ts +15 -0
  259. package/server/api/contests/[slug]/judge.post.ts +12 -0
  260. package/server/api/contests/[slug]/transition.post.ts +24 -0
  261. package/server/api/contests/index.get.ts +10 -0
  262. package/server/api/contests/index.post.ts +28 -0
  263. package/server/api/docs/[siteSlug]/index.delete.ts +14 -0
  264. package/server/api/docs/[siteSlug]/index.get.ts +17 -0
  265. package/server/api/docs/[siteSlug]/index.put.ts +20 -0
  266. package/server/api/docs/[siteSlug]/nav.get.ts +26 -0
  267. package/server/api/docs/[siteSlug]/pages/[pageId].delete.ts +14 -0
  268. package/server/api/docs/[siteSlug]/pages/[pageId].get.ts +31 -0
  269. package/server/api/docs/[siteSlug]/pages/[pageId].put.ts +15 -0
  270. package/server/api/docs/[siteSlug]/pages/index.get.ts +34 -0
  271. package/server/api/docs/[siteSlug]/pages/index.post.ts +28 -0
  272. package/server/api/docs/[siteSlug]/pages/reorder.post.ts +26 -0
  273. package/server/api/docs/[siteSlug]/search.get.ts +20 -0
  274. package/server/api/docs/[siteSlug]/versions.post.ts +11 -0
  275. package/server/api/docs/index.get.ts +6 -0
  276. package/server/api/docs/index.post.ts +10 -0
  277. package/server/api/federated-hubs/[id]/posts/[postId].get.ts +16 -0
  278. package/server/api/federated-hubs/[id]/posts.get.ts +15 -0
  279. package/server/api/federated-hubs/[id].get.ts +16 -0
  280. package/server/api/federation/boost.post.ts +21 -0
  281. package/server/api/federation/content/[id].get.ts +15 -0
  282. package/server/api/federation/follow.post.ts +16 -0
  283. package/server/api/federation/health.get.ts +56 -0
  284. package/server/api/federation/hub-follow.post.ts +27 -0
  285. package/server/api/federation/hub-post-like.post.ts +115 -0
  286. package/server/api/federation/hub-post-likes.get.ts +24 -0
  287. package/server/api/federation/hub-post-reply.post.ts +42 -0
  288. package/server/api/federation/hub-post.post.ts +33 -0
  289. package/server/api/federation/like.post.ts +21 -0
  290. package/server/api/federation/remote-actor.get.ts +22 -0
  291. package/server/api/federation/reply.post.ts +22 -0
  292. package/server/api/federation/search.post.ts +17 -0
  293. package/server/api/federation/timeline.get.ts +22 -0
  294. package/server/api/federation/unfollow.post.ts +17 -0
  295. package/server/api/files/[id].delete.ts +35 -0
  296. package/server/api/files/mine.get.ts +31 -0
  297. package/server/api/files/upload-from-url.post.ts +68 -0
  298. package/server/api/files/upload.post.ts +105 -0
  299. package/server/api/health.get.ts +4 -0
  300. package/server/api/hubs/[slug]/bans/[userId].delete.ts +13 -0
  301. package/server/api/hubs/[slug]/bans.get.ts +20 -0
  302. package/server/api/hubs/[slug]/bans.post.ts +23 -0
  303. package/server/api/hubs/[slug]/feed.xml.get.ts +60 -0
  304. package/server/api/hubs/[slug]/gallery.get.ts +21 -0
  305. package/server/api/hubs/[slug]/index.delete.ts +18 -0
  306. package/server/api/hubs/[slug]/index.get.ts +14 -0
  307. package/server/api/hubs/[slug]/index.put.ts +22 -0
  308. package/server/api/hubs/[slug]/invites.get.ts +20 -0
  309. package/server/api/hubs/[slug]/invites.post.ts +27 -0
  310. package/server/api/hubs/[slug]/join.post.ts +17 -0
  311. package/server/api/hubs/[slug]/leave.post.ts +13 -0
  312. package/server/api/hubs/[slug]/members/[userId].delete.ts +13 -0
  313. package/server/api/hubs/[slug]/members/[userId].put.ts +16 -0
  314. package/server/api/hubs/[slug]/members.get.ts +20 -0
  315. package/server/api/hubs/[slug]/posts/[postId]/like.post.ts +21 -0
  316. package/server/api/hubs/[slug]/posts/[postId]/lock.post.ts +17 -0
  317. package/server/api/hubs/[slug]/posts/[postId]/pin.post.ts +17 -0
  318. package/server/api/hubs/[slug]/posts/[postId]/replies.get.ts +16 -0
  319. package/server/api/hubs/[slug]/posts/[postId]/replies.post.ts +21 -0
  320. package/server/api/hubs/[slug]/posts/[postId].delete.ts +23 -0
  321. package/server/api/hubs/[slug]/posts/[postId].get.ts +16 -0
  322. package/server/api/hubs/[slug]/posts/index.get.ts +16 -0
  323. package/server/api/hubs/[slug]/posts/index.post.ts +36 -0
  324. package/server/api/hubs/[slug]/products.get.ts +26 -0
  325. package/server/api/hubs/[slug]/products.post.ts +18 -0
  326. package/server/api/hubs/[slug]/share.post.ts +39 -0
  327. package/server/api/hubs/index.get.ts +15 -0
  328. package/server/api/hubs/index.post.ts +11 -0
  329. package/server/api/image-proxy.get.ts +91 -0
  330. package/server/api/learn/[slug]/[lessonSlug]/complete.post.ts +13 -0
  331. package/server/api/learn/[slug]/[lessonSlug]/index.get.ts +68 -0
  332. package/server/api/learn/[slug]/enroll.post.ts +12 -0
  333. package/server/api/learn/[slug]/index.delete.ts +12 -0
  334. package/server/api/learn/[slug]/index.get.ts +14 -0
  335. package/server/api/learn/[slug]/index.put.ts +19 -0
  336. package/server/api/learn/[slug]/lessons/[lessonId].delete.ts +14 -0
  337. package/server/api/learn/[slug]/lessons/[lessonId].put.ts +24 -0
  338. package/server/api/learn/[slug]/lessons.post.ts +10 -0
  339. package/server/api/learn/[slug]/modules/[moduleId].delete.ts +14 -0
  340. package/server/api/learn/[slug]/modules/[moduleId].put.ts +15 -0
  341. package/server/api/learn/[slug]/modules.post.ts +14 -0
  342. package/server/api/learn/[slug]/publish.post.ts +13 -0
  343. package/server/api/learn/[slug]/unenroll.post.ts +12 -0
  344. package/server/api/learn/certificates.get.ts +9 -0
  345. package/server/api/learn/enrollments.get.ts +9 -0
  346. package/server/api/learn/index.get.ts +17 -0
  347. package/server/api/learn/index.post.ts +11 -0
  348. package/server/api/me.get.ts +13 -0
  349. package/server/api/messages/[conversationId]/info.get.ts +43 -0
  350. package/server/api/messages/[conversationId]/stream.get.ts +73 -0
  351. package/server/api/messages/[conversationId].get.ts +13 -0
  352. package/server/api/messages/[conversationId].post.ts +12 -0
  353. package/server/api/messages/index.get.ts +39 -0
  354. package/server/api/messages/index.post.ts +58 -0
  355. package/server/api/notifications/[id].delete.ts +11 -0
  356. package/server/api/notifications/count.get.ts +10 -0
  357. package/server/api/notifications/index.get.ts +24 -0
  358. package/server/api/notifications/read.post.ts +15 -0
  359. package/server/api/notifications/stream.get.ts +59 -0
  360. package/server/api/openapi.get.ts +5 -0
  361. package/server/api/products/[id].delete.ts +15 -0
  362. package/server/api/products/[id].put.ts +18 -0
  363. package/server/api/products/[slug]/content.get.ts +21 -0
  364. package/server/api/products/[slug].get.ts +16 -0
  365. package/server/api/products/index.get.ts +28 -0
  366. package/server/api/profile.get.ts +15 -0
  367. package/server/api/profile.put.ts +24 -0
  368. package/server/api/resolve-identity.post.ts +34 -0
  369. package/server/api/search/index.get.ts +24 -0
  370. package/server/api/search/trending.get.ts +22 -0
  371. package/server/api/social/bookmark.get.ts +16 -0
  372. package/server/api/social/bookmark.post.ts +15 -0
  373. package/server/api/social/bookmarks.get.ts +16 -0
  374. package/server/api/social/comments/[id].delete.ts +13 -0
  375. package/server/api/social/comments.get.ts +18 -0
  376. package/server/api/social/comments.post.ts +11 -0
  377. package/server/api/social/like.get.ts +17 -0
  378. package/server/api/social/like.post.ts +35 -0
  379. package/server/api/stats.get.ts +8 -0
  380. package/server/api/user/hubs.get.ts +22 -0
  381. package/server/api/users/[username]/content.get.ts +21 -0
  382. package/server/api/users/[username]/feed.xml.get.ts +65 -0
  383. package/server/api/users/[username]/follow.delete.ts +15 -0
  384. package/server/api/users/[username]/follow.post.ts +15 -0
  385. package/server/api/users/[username]/followers.get.ts +22 -0
  386. package/server/api/users/[username]/following.get.ts +22 -0
  387. package/server/api/users/[username]/learning.get.ts +18 -0
  388. package/server/api/users/[username].get.ts +25 -0
  389. package/server/api/users/index.get.ts +78 -0
  390. package/server/api/videos/[id].get.ts +18 -0
  391. package/server/api/videos/categories/[id].delete.ts +15 -0
  392. package/server/api/videos/categories/[id].put.ts +17 -0
  393. package/server/api/videos/categories.get.ts +7 -0
  394. package/server/api/videos/categories.post.ts +11 -0
  395. package/server/api/videos/index.get.ts +20 -0
  396. package/server/api/videos/index.post.ts +11 -0
  397. package/server/middleware/auth.ts +180 -0
  398. package/server/middleware/features.ts +31 -0
  399. package/server/middleware/security.ts +54 -0
  400. package/server/plugins/auto-admin.ts +69 -0
  401. package/server/plugins/federation-delivery.ts +74 -0
  402. package/server/routes/.well-known/nodeinfo.ts +15 -0
  403. package/server/routes/.well-known/webfinger.ts +86 -0
  404. package/server/routes/actor/followers.ts +34 -0
  405. package/server/routes/actor/following.ts +34 -0
  406. package/server/routes/actor/outbox.ts +31 -0
  407. package/server/routes/actor.ts +30 -0
  408. package/server/routes/feed.xml.ts +59 -0
  409. package/server/routes/hubs/[slug]/followers.ts +39 -0
  410. package/server/routes/hubs/[slug]/inbox.ts +35 -0
  411. package/server/routes/hubs/[slug]/outbox.ts +43 -0
  412. package/server/routes/hubs/[slug]/posts/[postId].ts +63 -0
  413. package/server/routes/hubs/[slug].ts +29 -0
  414. package/server/routes/inbox.ts +34 -0
  415. package/server/routes/nodeinfo/2.1.ts +27 -0
  416. package/server/routes/robots.txt.ts +19 -0
  417. package/server/routes/sitemap.xml.ts +105 -0
  418. package/server/routes/users/[username]/followers.ts +33 -0
  419. package/server/routes/users/[username]/following.ts +33 -0
  420. package/server/routes/users/[username]/inbox.ts +34 -0
  421. package/server/routes/users/[username]/outbox.ts +35 -0
  422. package/server/routes/users/[username].ts +62 -0
  423. package/server/utils/auth.ts +36 -0
  424. package/server/utils/db.ts +34 -0
  425. package/server/utils/errors.ts +24 -0
  426. package/server/utils/inbox.ts +122 -0
  427. package/server/utils/validate.ts +82 -0
  428. package/theme/base.css +283 -0
  429. package/theme/components.css +322 -0
  430. package/theme/dark.css +87 -0
  431. package/theme/editor-panels.css +63 -0
  432. package/theme/forms.css +216 -0
  433. package/theme/generics.css +68 -0
  434. package/theme/layouts.css +415 -0
  435. package/theme/prose.css +342 -0
  436. package/types/hub.ts +61 -0
@@ -0,0 +1,60 @@
1
+ import { getHubBySlug, listHubGallery } from '@commonpub/server';
2
+
3
+ function escapeXml(str: string): string {
4
+ return str
5
+ .replace(/&/g, '&')
6
+ .replace(/</g, '&lt;')
7
+ .replace(/>/g, '&gt;')
8
+ .replace(/"/g, '&quot;')
9
+ .replace(/'/g, '&apos;');
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 { slug } = parseParams(event, { slug: 'string' });
17
+
18
+
19
+ const hub = await getHubBySlug(db, slug);
20
+ if (!hub) {
21
+ throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
22
+ }
23
+
24
+ const { items } = await listHubGallery(db, hub.id, { limit: 50 });
25
+
26
+ const lastBuildDate = items.length > 0 && items[0].publishedAt
27
+ ? new Date(items[0].publishedAt).toUTCString()
28
+ : new Date().toUTCString();
29
+
30
+ const rssItems = items.map((item) => {
31
+ const link = `${siteUrl}/${item.type}/${item.slug}`;
32
+ const pubDate = item.publishedAt ? new Date(item.publishedAt).toUTCString() : new Date().toUTCString();
33
+ return ` <item>
34
+ <title>${escapeXml(item.title)}</title>
35
+ <link>${escapeXml(link)}</link>
36
+ <guid isPermaLink="true">${escapeXml(link)}</guid>
37
+ <pubDate>${pubDate}</pubDate>
38
+ <description>${escapeXml((item as any).description ?? item.title)}</description>
39
+ <author>${escapeXml(item.author.displayName ?? item.author.username)}</author>
40
+ <category>${escapeXml(item.type)}</category>
41
+ </item>`;
42
+ });
43
+
44
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
45
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
46
+ <channel>
47
+ <title>${escapeXml(hub.name)} — CommonPub</title>
48
+ <link>${escapeXml(siteUrl)}/hubs/${escapeXml(slug)}</link>
49
+ <description>${escapeXml(hub.description ?? `Content from ${hub.name}`)}</description>
50
+ <language>en</language>
51
+ <lastBuildDate>${lastBuildDate}</lastBuildDate>
52
+ <atom:link href="${escapeXml(siteUrl)}/api/hubs/${escapeXml(slug)}/feed.xml" rel="self" type="application/rss+xml"/>
53
+ ${rssItems.join('\n')}
54
+ </channel>
55
+ </rss>`;
56
+
57
+ setResponseHeader(event, 'Content-Type', 'application/rss+xml; charset=utf-8');
58
+ setResponseHeader(event, 'Cache-Control', 'public, max-age=600, stale-while-revalidate=300');
59
+ return xml;
60
+ });
@@ -0,0 +1,21 @@
1
+ import { getHubBySlug, listHubGallery } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const galleryQuerySchema = z.object({
5
+ limit: z.coerce.number().int().positive().max(100).optional(),
6
+ offset: z.coerce.number().int().min(0).optional(),
7
+ });
8
+
9
+ export default defineEventHandler(async (event) => {
10
+ const db = useDB();
11
+ const { slug } = parseParams(event, { slug: 'string' });
12
+ const query = parseQueryParams(event, galleryQuerySchema);
13
+
14
+
15
+ const hub = await getHubBySlug(db, slug);
16
+ if (!hub) {
17
+ throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
18
+ }
19
+
20
+ return listHubGallery(db, hub.id, query);
21
+ });
@@ -0,0 +1,18 @@
1
+ import { deleteHub, getHubBySlug } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { slug } = parseParams(event, { slug: 'string' });
7
+
8
+ const hub = await getHubBySlug(db, slug, user.id);
9
+ if (!hub) {
10
+ throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
11
+ }
12
+
13
+ const deleted = await deleteHub(db, hub.id, user.id);
14
+ if (!deleted) {
15
+ throw createError({ statusCode: 403, statusMessage: 'Not authorized to delete this hub' });
16
+ }
17
+ return { success: true };
18
+ });
@@ -0,0 +1,14 @@
1
+ import { getHubBySlug } from '@commonpub/server';
2
+ import type { HubDetail } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<HubDetail> => {
5
+ const db = useDB();
6
+ const { slug } = parseParams(event, { slug: 'string' });
7
+ const user = getOptionalUser(event);
8
+
9
+ const community = await getHubBySlug(db, slug, user?.id);
10
+ if (!community) {
11
+ throw createError({ statusCode: 404, statusMessage: 'Community not found' });
12
+ }
13
+ return community;
14
+ });
@@ -0,0 +1,22 @@
1
+ import { updateHub, getHubBySlug } from '@commonpub/server';
2
+ import type { HubDetail } from '@commonpub/server';
3
+ import { updateHubSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<HubDetail> => {
6
+ const user = requireAuth(event);
7
+ const db = useDB();
8
+ const { slug } = parseParams(event, { slug: 'string' });
9
+
10
+ const hub = await getHubBySlug(db, slug, user.id);
11
+ if (!hub) {
12
+ throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
13
+ }
14
+
15
+ const input = await parseBody(event, updateHubSchema);
16
+
17
+ const updated = await updateHub(db, hub.id, user.id, input);
18
+ if (!updated) {
19
+ throw createError({ statusCode: 403, statusMessage: 'Not authorized to update this hub' });
20
+ }
21
+ return updated;
22
+ });
@@ -0,0 +1,20 @@
1
+ import { listInvites, getHubBySlug, getMember } from '@commonpub/server';
2
+ import type { HubInviteItem } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<HubInviteItem[]> => {
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 invite 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 listInvites(db, hub.id);
20
+ });
@@ -0,0 +1,27 @@
1
+ import { createInvite, getHubBySlug } from '@commonpub/server';
2
+ import type { HubInviteItem } from '@commonpub/server';
3
+ import { createInviteSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<HubInviteItem> => {
6
+ const user = requireAuth(event);
7
+ const db = useDB();
8
+ const { slug } = parseParams(event, { slug: 'string' });
9
+ const input = await parseBody(event, createInviteSchema);
10
+
11
+ const hub = await getHubBySlug(db, slug);
12
+ if (!hub) {
13
+ throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
14
+ }
15
+
16
+ const invite = await createInvite(
17
+ db,
18
+ user.id,
19
+ hub.id,
20
+ input.maxUses,
21
+ input.expiresAt ? new Date(input.expiresAt) : undefined,
22
+ );
23
+ if (!invite) {
24
+ throw createError({ statusCode: 403, statusMessage: 'Not authorized to create invites' });
25
+ }
26
+ return invite;
27
+ });
@@ -0,0 +1,17 @@
1
+ import { joinHub, getHubBySlug } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<{ joined: boolean; error?: string }> => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { slug } = parseParams(event, { slug: 'string' });
7
+ const community = await getHubBySlug(db, slug);
8
+ if (!community) {
9
+ throw createError({ statusCode: 404, statusMessage: 'Community not found' });
10
+ }
11
+
12
+ // Read optional invite token from body (required for non-open hubs)
13
+ const body = await readBody(event).catch(() => null);
14
+ const inviteToken = typeof body?.inviteToken === 'string' ? body.inviteToken : undefined;
15
+
16
+ return joinHub(db, user.id, community.id, inviteToken);
17
+ });
@@ -0,0 +1,13 @@
1
+ import { leaveHub, getHubBySlug } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<{ left: boolean; error?: string }> => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { slug } = parseParams(event, { slug: 'string' });
7
+ const community = await getHubBySlug(db, slug);
8
+ if (!community) {
9
+ throw createError({ statusCode: 404, statusMessage: 'Community not found' });
10
+ }
11
+
12
+ return leaveHub(db, user.id, community.id);
13
+ });
@@ -0,0 +1,13 @@
1
+ import { kickMember, getHubBySlug } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<{ kicked: 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 kickMember(db, user.id, community.id, userId);
13
+ });
@@ -0,0 +1,16 @@
1
+ import { changeRole, getHubBySlug } from '@commonpub/server';
2
+ import { changeRoleSchema } from '@commonpub/schema';
3
+
4
+ export default defineEventHandler(async (event): Promise<{ changed: boolean; error?: string }> => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+ const { slug, userId } = parseParams(event, { slug: 'string', userId: 'uuid' });
8
+ const input = await parseBody(event, changeRoleSchema);
9
+
10
+ const hub = await getHubBySlug(db, slug);
11
+ if (!hub) {
12
+ throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
13
+ }
14
+
15
+ return changeRole(db, user.id, hub.id, userId, input.role);
16
+ });
@@ -0,0 +1,20 @@
1
+ import { listMembers, getHubBySlug } from '@commonpub/server';
2
+ import type { HubMemberItem } from '@commonpub/server';
3
+ import { z } from 'zod';
4
+
5
+ const membersQuerySchema = z.object({
6
+ limit: z.coerce.number().int().min(1).max(100).optional(),
7
+ offset: z.coerce.number().int().min(0).optional(),
8
+ });
9
+
10
+ export default defineEventHandler(async (event): Promise<{ items: HubMemberItem[]; total: number }> => {
11
+ const db = useDB();
12
+ const { slug } = parseParams(event, { slug: 'string' });
13
+ const query = parseQueryParams(event, membersQuerySchema);
14
+ const community = await getHubBySlug(db, slug);
15
+ if (!community) {
16
+ throw createError({ statusCode: 404, statusMessage: 'Community not found' });
17
+ }
18
+
19
+ return listMembers(db, community.id, query);
20
+ });
@@ -0,0 +1,21 @@
1
+ import { likePost, unlikePost, hasLikedPost, getHubBySlug, getPostById } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { slug, postId } = parseParams(event, { slug: 'string', postId: 'uuid' });
7
+
8
+ const community = await getHubBySlug(db, slug);
9
+ if (!community) throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
10
+
11
+ const post = await getPostById(db, postId);
12
+ if (!post || post.hubId !== community.id) throw createError({ statusCode: 404, statusMessage: 'Post not found' });
13
+
14
+ const alreadyLiked = await hasLikedPost(db, user.id, postId);
15
+ if (alreadyLiked) {
16
+ await unlikePost(db, user.id, postId);
17
+ return { liked: false };
18
+ }
19
+ await likePost(db, user.id, postId);
20
+ return { liked: true };
21
+ });
@@ -0,0 +1,17 @@
1
+ import { toggleLockPost, getHubBySlug, getPostById } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { slug, postId } = parseParams(event, { slug: 'string', postId: 'uuid' });
7
+
8
+ const community = await getHubBySlug(db, slug);
9
+ if (!community) throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
10
+
11
+ const post = await getPostById(db, postId);
12
+ if (!post || post.hubId !== community.id) throw createError({ statusCode: 404, statusMessage: 'Post not found' });
13
+
14
+ const result = await toggleLockPost(db, postId, user.id, community.id);
15
+ if (!result) throw createError({ statusCode: 403, statusMessage: 'Not authorized' });
16
+ return result;
17
+ });
@@ -0,0 +1,17 @@
1
+ import { togglePinPost, getHubBySlug, getPostById } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { slug, postId } = parseParams(event, { slug: 'string', postId: 'uuid' });
7
+
8
+ const community = await getHubBySlug(db, slug);
9
+ if (!community) throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
10
+
11
+ const post = await getPostById(db, postId);
12
+ if (!post || post.hubId !== community.id) throw createError({ statusCode: 404, statusMessage: 'Post not found' });
13
+
14
+ const result = await togglePinPost(db, postId, user.id, community.id);
15
+ if (!result) throw createError({ statusCode: 403, statusMessage: 'Not authorized' });
16
+ return result;
17
+ });
@@ -0,0 +1,16 @@
1
+ import { listReplies } from '@commonpub/server';
2
+ import type { HubReplyItem } from '@commonpub/server';
3
+ import { z } from 'zod';
4
+
5
+ const repliesQuerySchema = z.object({
6
+ limit: z.coerce.number().int().min(1).max(100).optional(),
7
+ offset: z.coerce.number().int().min(0).optional(),
8
+ });
9
+
10
+ export default defineEventHandler(async (event): Promise<{ items: HubReplyItem[]; total: number }> => {
11
+ const db = useDB();
12
+ const { postId } = parseParams(event, { postId: 'uuid' });
13
+ const query = parseQueryParams(event, repliesQuerySchema);
14
+
15
+ return listReplies(db, postId, query);
16
+ });
@@ -0,0 +1,21 @@
1
+ import { createReply } from '@commonpub/server';
2
+ import type { HubReplyItem } from '@commonpub/server';
3
+ import { createReplySchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<HubReplyItem> => {
6
+ const user = requireAuth(event);
7
+ const db = useDB();
8
+ const { postId } = parseParams(event, { postId: 'uuid' });
9
+ const body = await readBody(event);
10
+
11
+ const parsed = createReplySchema.safeParse({ ...body, postId });
12
+ if (!parsed.success) {
13
+ throw createError({
14
+ statusCode: 400,
15
+ statusMessage: 'Validation failed',
16
+ data: { errors: parsed.error.flatten().fieldErrors },
17
+ });
18
+ }
19
+
20
+ return createReply(db, user.id, parsed.data);
21
+ });
@@ -0,0 +1,23 @@
1
+ import { deletePost, getHubBySlug, federateHubPostDelete } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<boolean> => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const config = useConfig();
7
+ const { slug, postId } = parseParams(event, { slug: 'string', postId: 'uuid' });
8
+ const community = await getHubBySlug(db, slug);
9
+ if (!community) {
10
+ throw createError({ statusCode: 404, statusMessage: 'Community not found' });
11
+ }
12
+
13
+ const deleted = await deletePost(db, postId, user.id, community.id);
14
+
15
+ // Federate deletion as Delete(Tombstone) from the hub Group actor
16
+ if (deleted && config.features.federation && config.features.federateHubs) {
17
+ federateHubPostDelete(db, postId, slug, config.instance.domain).catch((err) => {
18
+ console.error('[hub-federation] Failed to federate post deletion:', err);
19
+ });
20
+ }
21
+
22
+ return deleted;
23
+ });
@@ -0,0 +1,16 @@
1
+ import { getPostById, getHubBySlug, hasLikedPost } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const db = useDB();
5
+ const user = getOptionalUser(event);
6
+ const { slug, postId } = parseParams(event, { slug: 'string', postId: 'uuid' });
7
+
8
+ const community = await getHubBySlug(db, slug);
9
+ if (!community) throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
10
+
11
+ const post = await getPostById(db, postId);
12
+ if (!post || post.hubId !== community.id) throw createError({ statusCode: 404, statusMessage: 'Post not found' });
13
+
14
+ const liked = user ? await hasLikedPost(db, user.id, postId) : false;
15
+ return { ...post, isLiked: liked };
16
+ });
@@ -0,0 +1,16 @@
1
+ import { listPosts, getHubBySlug } from '@commonpub/server';
2
+ import type { PaginatedResponse, HubPostItem } from '@commonpub/server';
3
+ import { hubPostFiltersSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<PaginatedResponse<HubPostItem>> => {
6
+ const db = useDB();
7
+ const { slug } = parseParams(event, { slug: 'string' });
8
+ const filters = parseQueryParams(event, hubPostFiltersSchema);
9
+
10
+ const hub = await getHubBySlug(db, slug);
11
+ if (!hub) {
12
+ throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
13
+ }
14
+
15
+ return listPosts(db, hub.id, filters);
16
+ });
@@ -0,0 +1,36 @@
1
+ import { createPost, getHubBySlug, federateHubPost } from '@commonpub/server';
2
+ import type { HubPostItem } from '@commonpub/server';
3
+ import { createPostSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<HubPostItem> => {
6
+ const user = requireAuth(event);
7
+ const db = useDB();
8
+ const config = useConfig();
9
+ const { slug } = parseParams(event, { slug: 'string' });
10
+
11
+ const community = await getHubBySlug(db, slug);
12
+ if (!community) {
13
+ throw createError({ statusCode: 404, statusMessage: 'Community not found' });
14
+ }
15
+
16
+ const body = await readBody(event);
17
+ const parsed = createPostSchema.safeParse({ ...body, hubId: community.id });
18
+ if (!parsed.success) {
19
+ throw createError({
20
+ statusCode: 400,
21
+ statusMessage: 'Validation failed',
22
+ data: { errors: parsed.error.flatten().fieldErrors },
23
+ });
24
+ }
25
+
26
+ const post = await createPost(db, user.id, { hubId: community.id, type: parsed.data.type, content: parsed.data.content });
27
+
28
+ // Federate hub post as Announce from Group actor (fire-and-forget)
29
+ if (config.features.federation && config.features.federateHubs) {
30
+ federateHubPost(db, post.id, community.id, config.instance.domain).catch((err) => {
31
+ console.error('[hub-federation] Failed to federate post:', err);
32
+ });
33
+ }
34
+
35
+ return post;
36
+ });
@@ -0,0 +1,26 @@
1
+ import { getHubBySlug, listHubProducts } from '@commonpub/server';
2
+ import type { PaginatedResponse, ProductListItem } from '@commonpub/server';
3
+ import { z } from 'zod';
4
+ import { productStatusSchema, productCategorySchema } from '@commonpub/schema';
5
+
6
+ const productQuerySchema = z.object({
7
+ search: z.string().max(200).optional(),
8
+ category: productCategorySchema.optional(),
9
+ status: productStatusSchema.optional(),
10
+ limit: z.coerce.number().int().positive().max(100).optional(),
11
+ offset: z.coerce.number().int().min(0).optional(),
12
+ });
13
+
14
+ export default defineEventHandler(async (event): Promise<PaginatedResponse<ProductListItem>> => {
15
+ const db = useDB();
16
+ const { slug } = parseParams(event, { slug: 'string' });
17
+ const filters = parseQueryParams(event, productQuerySchema);
18
+
19
+
20
+ const hub = await getHubBySlug(db, slug);
21
+ if (!hub) {
22
+ throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
23
+ }
24
+
25
+ return listHubProducts(db, hub.id, filters);
26
+ });
@@ -0,0 +1,18 @@
1
+ import { getHubBySlug, createProduct } from '@commonpub/server';
2
+ import type { ProductDetail } from '@commonpub/server';
3
+ import { createProductSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<ProductDetail> => {
6
+ const db = useDB();
7
+ const user = requireAuth(event);
8
+ const { slug } = parseParams(event, { slug: 'string' });
9
+
10
+ const hub = await getHubBySlug(db, slug, user.id);
11
+ if (!hub) {
12
+ throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
13
+ }
14
+
15
+ const input = await parseBody(event, createProductSchema);
16
+
17
+ return createProduct(db, user.id, hub.id, input);
18
+ });
@@ -0,0 +1,39 @@
1
+ import { shareContent, getHubBySlug, federateHubShare, getContentSlugById, buildContentUri } from '@commonpub/server';
2
+ import type { HubPostItem } from '@commonpub/server';
3
+ import { z } from 'zod';
4
+
5
+ const shareContentSchema = z.object({
6
+ contentId: z.string().uuid(),
7
+ });
8
+
9
+ export default defineEventHandler(async (event): Promise<HubPostItem> => {
10
+ const user = requireAuth(event);
11
+ const db = useDB();
12
+ const config = useConfig();
13
+ const { slug } = parseParams(event, { slug: 'string' });
14
+ const input = await parseBody(event, shareContentSchema);
15
+
16
+ const hub = await getHubBySlug(db, slug);
17
+ if (!hub) {
18
+ throw createError({ statusCode: 404, statusMessage: 'Hub not found' });
19
+ }
20
+
21
+ const post = await shareContent(db, user.id, hub.id, input.contentId);
22
+ if (!post) {
23
+ throw createError({ statusCode: 400, statusMessage: 'Cannot share. You must be a hub member and the content must exist.' });
24
+ }
25
+
26
+ // Federate the shared content as Announce from the hub Group actor
27
+ if (config.features.federation && config.features.federateHubs) {
28
+ getContentSlugById(db, input.contentId).then((contentSlug) => {
29
+ if (contentSlug) {
30
+ const contentUri = buildContentUri(config.instance.domain, contentSlug);
31
+ return federateHubShare(db, contentUri, hub.id, config.instance.domain);
32
+ }
33
+ }).catch((err) => {
34
+ console.error('[hub-federation] Failed to federate share:', err);
35
+ });
36
+ }
37
+
38
+ return post;
39
+ });
@@ -0,0 +1,15 @@
1
+ import { listHubs } from '@commonpub/server';
2
+ import type { HubListItem, FederatedHubListItem } from '@commonpub/server';
3
+ import { hubFiltersSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<{ items: (HubListItem | FederatedHubListItem)[]; total: number }> => {
6
+ const db = useDB();
7
+ const config = useConfig();
8
+ const filters = parseQueryParams(event, hubFiltersSchema);
9
+
10
+ // seamlessFederation controls display of mirrored content (including hubs from other instances)
11
+ // federateHubs controls outbound hub federation (sending our hubs) — not needed for display
12
+ const includeFederated = !!config.features.seamlessFederation;
13
+
14
+ return listHubs(db, filters, { includeFederated });
15
+ });
@@ -0,0 +1,11 @@
1
+ import { createHub } from '@commonpub/server';
2
+ import type { HubDetail } from '@commonpub/server';
3
+ import { createHubSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<HubDetail> => {
6
+ const user = requireAuth(event);
7
+ const db = useDB();
8
+ const input = await parseBody(event, createHubSchema);
9
+
10
+ return createHub(db, user.id, input);
11
+ });