@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,12 @@
1
+ import { updateUserRole } from '@commonpub/server';
2
+ import { adminUpdateRoleSchema } from '@commonpub/schema';
3
+
4
+ export default defineEventHandler(async (event): Promise<void> => {
5
+ requireFeature('admin');
6
+ const admin = requireAdmin(event);
7
+ const db = useDB();
8
+ const { id } = parseParams(event, { id: 'uuid' });
9
+ const input = await parseBody(event, adminUpdateRoleSchema);
10
+
11
+ return updateUserRole(db, id, input.role, admin.id);
12
+ });
@@ -0,0 +1,12 @@
1
+ import { updateUserStatus } from '@commonpub/server';
2
+ import { adminUpdateStatusSchema } from '@commonpub/schema';
3
+
4
+ export default defineEventHandler(async (event): Promise<void> => {
5
+ requireFeature('admin');
6
+ const admin = requireAdmin(event);
7
+ const db = useDB();
8
+ const { id } = parseParams(event, { id: 'uuid' });
9
+ const input = await parseBody(event, adminUpdateStatusSchema);
10
+
11
+ return updateUserStatus(db, id, input.status, admin.id);
12
+ });
@@ -0,0 +1,10 @@
1
+ import { deleteUser } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<void> => {
4
+ requireFeature('admin');
5
+ const admin = requireAdmin(event);
6
+ const db = useDB();
7
+ const { id } = parseParams(event, { id: 'uuid' });
8
+
9
+ return deleteUser(db, id, admin.id);
10
+ });
@@ -0,0 +1,18 @@
1
+ import { listUsers } from '@commonpub/server';
2
+ import type { PaginatedResponse, UserListItem } from '@commonpub/server';
3
+ import { z } from 'zod';
4
+
5
+ const adminUsersQuerySchema = z.object({
6
+ search: z.string().max(200).optional(),
7
+ limit: z.coerce.number().int().positive().max(100).optional(),
8
+ offset: z.coerce.number().int().min(0).optional(),
9
+ });
10
+
11
+ export default defineEventHandler(async (event): Promise<PaginatedResponse<UserListItem>> => {
12
+ requireFeature('admin');
13
+ requireAdmin(event);
14
+ const db = useDB();
15
+ const filters = parseQueryParams(event, adminUsersQuerySchema);
16
+
17
+ return listUsers(db, filters);
18
+ });
@@ -0,0 +1,67 @@
1
+ import { consumeOAuthState, exchangeCodeForToken, linkFederatedAccount } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const callbackSchema = z.object({
5
+ code: z.string(),
6
+ state: z.string(),
7
+ });
8
+
9
+ /**
10
+ * OAuth2 callback handler for federated login.
11
+ * Exchanges authorization code for token, links federated account.
12
+ */
13
+ export default defineEventHandler(async (event) => {
14
+ requireFeature('federation');
15
+ const db = useDB();
16
+ const { code, state: stateToken } = parseQueryParams(event, callbackSchema);
17
+
18
+ // Retrieve and consume the stored OAuth state (single-use, 10min TTL)
19
+ const oauthState = await consumeOAuthState(db, stateToken);
20
+ if (!oauthState) {
21
+ throw createError({
22
+ statusCode: 400,
23
+ statusMessage: 'Invalid or expired OAuth state. Please try logging in again.',
24
+ });
25
+ }
26
+
27
+ // Exchange the authorization code for an access token + user info
28
+ const tokenResult = await exchangeCodeForToken(oauthState, code);
29
+ if (!tokenResult) {
30
+ throw createError({
31
+ statusCode: 502,
32
+ statusMessage: 'Failed to exchange authorization code with the remote instance.',
33
+ });
34
+ }
35
+
36
+ // Check if a local user is already linked to this federated account
37
+ const { findUserByFederatedAccount } = await import('@commonpub/server');
38
+ const existingLink = await findUserByFederatedAccount(db, tokenResult.user.actorUri);
39
+
40
+ if (existingLink) {
41
+ // User already linked — redirect to dashboard
42
+ // In a full implementation, this would also create a Better Auth session
43
+ return sendRedirect(event, `/dashboard?federated=linked&user=${existingLink.username}`, 302);
44
+ }
45
+
46
+ // Check if the current user is logged in — if so, link to their account
47
+ const auth = event.context.auth;
48
+ if (auth?.user) {
49
+ await linkFederatedAccount(db, auth.user.id, tokenResult.user.actorUri, oauthState.instanceDomain, {
50
+ preferredUsername: tokenResult.user.username,
51
+ displayName: tokenResult.user.displayName ?? undefined,
52
+ avatarUrl: tokenResult.user.avatarUrl ?? undefined,
53
+ });
54
+
55
+ return sendRedirect(event, '/settings/account?federated=linked', 302);
56
+ }
57
+
58
+ // Not logged in and no existing link — redirect to login page with federated context
59
+ // The user needs to either create an account or log in to link
60
+ const params = new URLSearchParams({
61
+ federated: 'true',
62
+ actorUri: tokenResult.user.actorUri,
63
+ username: tokenResult.user.username,
64
+ instance: oauthState.instanceDomain,
65
+ });
66
+ return sendRedirect(event, `/auth/login?${params.toString()}`, 302);
67
+ });
@@ -0,0 +1,60 @@
1
+ import { discoverOAuthEndpoint, isTrustedInstance } from '@commonpub/auth';
2
+ import { storeOAuthState } from '@commonpub/server';
3
+ import { z } from 'zod';
4
+
5
+ const loginSchema = z.object({
6
+ instanceDomain: z.string().min(3).max(255),
7
+ /** Client credentials — in production these come from admin-registered clients */
8
+ clientId: z.string().optional(),
9
+ clientSecret: z.string().optional(),
10
+ });
11
+
12
+ /**
13
+ * Initiate federated login. Discovers OAuth endpoints via WebFinger,
14
+ * stores state for callback, returns authorization URL.
15
+ */
16
+ export default defineEventHandler(async (event) => {
17
+ requireFeature('federation');
18
+ const config = useConfig();
19
+ const db = useDB();
20
+ const { instanceDomain, clientId, clientSecret } = await parseBody(event, loginSchema);
21
+
22
+ if (!isTrustedInstance(config, instanceDomain)) {
23
+ throw createError({
24
+ statusCode: 403,
25
+ statusMessage: `Instance ${instanceDomain} is not in the trusted instances list`,
26
+ });
27
+ }
28
+
29
+ const endpoints = await discoverOAuthEndpoint(instanceDomain, 'instance');
30
+ if (!endpoints) {
31
+ throw createError({
32
+ statusCode: 502,
33
+ statusMessage: `Could not discover OAuth endpoints for ${instanceDomain}`,
34
+ });
35
+ }
36
+
37
+ const redirectUri = `https://${config.instance.domain}/api/auth/federated/callback`;
38
+ const effectiveClientId = clientId ?? `cpub_${config.instance.domain}`;
39
+ const effectiveClientSecret = clientSecret ?? '';
40
+
41
+ // Store state for the callback handler
42
+ const stateToken = await storeOAuthState(db, {
43
+ tokenEndpoint: endpoints.tokenEndpoint,
44
+ clientId: effectiveClientId,
45
+ clientSecret: effectiveClientSecret,
46
+ redirectUri,
47
+ instanceDomain,
48
+ });
49
+
50
+ const authUrl = new URL(endpoints.authorizationEndpoint);
51
+ authUrl.searchParams.set('client_id', effectiveClientId);
52
+ authUrl.searchParams.set('redirect_uri', redirectUri);
53
+ authUrl.searchParams.set('response_type', 'code');
54
+ authUrl.searchParams.set('state', stateToken);
55
+
56
+ return {
57
+ authorizationUrl: authUrl.toString(),
58
+ state: stateToken,
59
+ };
60
+ });
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod';
2
+
3
+ const authorizeQuerySchema = z.object({
4
+ client_id: z.string(),
5
+ redirect_uri: z.string().url(),
6
+ response_type: z.string(),
7
+ scope: z.string().optional(),
8
+ state: z.string().optional(),
9
+ });
10
+
11
+ /**
12
+ * OAuth2 authorize endpoint (GET).
13
+ * Returns the authorization parameters for the consent page to render.
14
+ * The actual authorization happens via POST after user consents.
15
+ */
16
+ export default defineEventHandler(async (event) => {
17
+ requireFeature('federation');
18
+ const user = requireAuth(event);
19
+ const query = parseQueryParams(event, authorizeQuerySchema);
20
+
21
+ // Return params for the consent page to display
22
+ return {
23
+ clientId: query.client_id,
24
+ redirectUri: query.redirect_uri,
25
+ responseType: query.response_type,
26
+ scope: query.scope,
27
+ state: query.state,
28
+ user: { username: user.username },
29
+ };
30
+ });
@@ -0,0 +1,51 @@
1
+ import { processAuthorize } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const authorizeSchema = z.object({
5
+ clientId: z.string(),
6
+ redirectUri: z.string().url(),
7
+ responseType: z.string(),
8
+ scope: z.string().optional(),
9
+ state: z.string().optional(),
10
+ });
11
+
12
+ /**
13
+ * OAuth2 authorize endpoint (POST).
14
+ * Called when user approves the consent screen.
15
+ * Generates auth code and returns redirect URL.
16
+ */
17
+ export default defineEventHandler(async (event) => {
18
+ requireFeature('federation');
19
+ const user = requireAuth(event);
20
+ const db = useDB();
21
+ const config = useConfig();
22
+ const input = await parseBody(event, authorizeSchema);
23
+
24
+ const result = await processAuthorize(
25
+ db,
26
+ {
27
+ clientId: input.clientId,
28
+ redirectUri: input.redirectUri,
29
+ responseType: input.responseType,
30
+ scope: input.scope,
31
+ state: input.state,
32
+ },
33
+ user.id,
34
+ config.instance.domain,
35
+ );
36
+
37
+ if ('error' in result) {
38
+ throw createError({
39
+ statusCode: 400,
40
+ statusMessage: result.errorDescription,
41
+ data: { error: result.error },
42
+ });
43
+ }
44
+
45
+ // Build redirect URL with code and state
46
+ const redirectUrl = new URL(result.redirectUri);
47
+ redirectUrl.searchParams.set('code', result.code);
48
+ if (result.state) redirectUrl.searchParams.set('state', result.state);
49
+
50
+ return { redirectUrl: redirectUrl.toString() };
51
+ });
@@ -0,0 +1,41 @@
1
+ import { processDynamicRegistration } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const registerSchema = z.object({
5
+ client_name: z.string().min(1).max(255),
6
+ redirect_uris: z.array(z.string().url()).min(1),
7
+ client_uri: z.string().url().optional(),
8
+ instance_domain: z.string().min(3).max(255),
9
+ });
10
+
11
+ /**
12
+ * Dynamic OAuth client registration endpoint.
13
+ * Allows remote CommonPub instances to auto-register for SSO.
14
+ * No auth required — the whole point is zero-friction instance-to-instance setup.
15
+ */
16
+ export default defineEventHandler(async (event) => {
17
+ requireFeature('federation');
18
+ const db = useDB();
19
+ const input = await parseBody(event, registerSchema);
20
+
21
+ const result = await processDynamicRegistration(db, {
22
+ clientName: input.client_name,
23
+ redirectUris: input.redirect_uris,
24
+ clientUri: input.client_uri,
25
+ instanceDomain: input.instance_domain,
26
+ });
27
+
28
+ if ('error' in result) {
29
+ throw createError({
30
+ statusCode: 400,
31
+ statusMessage: result.errorDescription,
32
+ data: { error: result.error },
33
+ });
34
+ }
35
+
36
+ return {
37
+ client_id: result.clientId,
38
+ client_secret: result.clientSecret,
39
+ registration_type: 'dynamic',
40
+ };
41
+ });
@@ -0,0 +1,48 @@
1
+ import { processTokenExchange } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const tokenSchema = z.object({
5
+ grant_type: z.string(),
6
+ code: z.string(),
7
+ client_id: z.string(),
8
+ client_secret: z.string(),
9
+ redirect_uri: z.string().url(),
10
+ });
11
+
12
+ /**
13
+ * OAuth2 token endpoint.
14
+ * Exchanges authorization code for access token + user info.
15
+ */
16
+ export default defineEventHandler(async (event) => {
17
+ requireFeature('federation');
18
+ const db = useDB();
19
+ const config = useConfig();
20
+ const input = await parseBody(event, tokenSchema);
21
+
22
+ const result = await processTokenExchange(
23
+ db,
24
+ {
25
+ grantType: input.grant_type,
26
+ code: input.code,
27
+ clientId: input.client_id,
28
+ clientSecret: input.client_secret,
29
+ redirectUri: input.redirect_uri,
30
+ },
31
+ config.instance.domain,
32
+ );
33
+
34
+ if ('error' in result) {
35
+ throw createError({
36
+ statusCode: 400,
37
+ statusMessage: result.errorDescription,
38
+ data: { error: result.error },
39
+ });
40
+ }
41
+
42
+ return {
43
+ access_token: result.accessToken,
44
+ token_type: result.tokenType,
45
+ expires_in: result.expiresIn,
46
+ user: result.user,
47
+ };
48
+ });
@@ -0,0 +1,13 @@
1
+ import { getCertificateByCode } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const db = useDB();
5
+ const { code } = parseParams(event, { code: 'string' });
6
+
7
+ const result = await getCertificateByCode(db, code);
8
+ if (!result) {
9
+ throw createError({ statusCode: 404, statusMessage: 'Certificate not found' });
10
+ }
11
+
12
+ return result;
13
+ });
@@ -0,0 +1,9 @@
1
+ import { toggleBuildMark } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<{ marked: boolean; count: number }> => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const { id } = parseParams(event, { id: 'uuid' });
7
+
8
+ return toggleBuildMark(db, id, user.id);
9
+ });
@@ -0,0 +1,10 @@
1
+ import { forkContent } from '@commonpub/server';
2
+ import type { ContentDetail } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<ContentDetail> => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+ const { id } = parseParams(event, { id: 'uuid' });
8
+
9
+ return forkContent(db, id, user.id);
10
+ });
@@ -0,0 +1,18 @@
1
+ import { deleteContent, onContentDeleted } from '@commonpub/server';
2
+
3
+ export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
4
+ const user = requireAuth(event);
5
+ const db = useDB();
6
+ const config = useConfig();
7
+ const { id } = parseParams(event, { id: 'uuid' });
8
+
9
+ const deleted = await deleteContent(db, id, user.id);
10
+ if (!deleted) {
11
+ throw createError({ statusCode: 404, statusMessage: 'Content not found or not owned by you' });
12
+ }
13
+
14
+ // Federate the deletion (content row still exists as soft-deleted, slug available)
15
+ await onContentDeleted(db, id, user.username, config);
16
+
17
+ return { success: true };
18
+ });
@@ -0,0 +1,15 @@
1
+ import { getContentBySlug } from '@commonpub/server';
2
+ import type { ContentDetail } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<ContentDetail> => {
5
+ const db = useDB();
6
+ // Param is named 'id' (directory name) but the value is a slug for GET requests
7
+ const { id: slugOrId } = parseParams(event, { id: 'string' });
8
+ const user = getOptionalUser(event);
9
+
10
+ const content = await getContentBySlug(db, slugOrId, user?.id);
11
+ if (!content) {
12
+ throw createError({ statusCode: 404, statusMessage: 'Content not found' });
13
+ }
14
+ return content;
15
+ });
@@ -0,0 +1,23 @@
1
+ import { updateContent, onContentUpdated } from '@commonpub/server';
2
+ import type { ContentDetail } from '@commonpub/server';
3
+ import { updateContentSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<ContentDetail> => {
6
+ const user = requireAuth(event);
7
+ const db = useDB();
8
+ const config = useConfig();
9
+ const { id } = parseParams(event, { id: 'uuid' });
10
+ const input = await parseBody(event, updateContentSchema);
11
+
12
+ const content = await updateContent(db, id, user.id, input);
13
+ if (!content) {
14
+ throw createError({ statusCode: 404, statusMessage: 'Content not found or not owned by you' });
15
+ }
16
+
17
+ // Only federate updates to published content
18
+ if (content.status === 'published') {
19
+ await onContentUpdated(db, id, config);
20
+ }
21
+
22
+ return content;
23
+ });
@@ -0,0 +1,28 @@
1
+ import { removeContentProduct } from '@commonpub/server';
2
+ import { eq, and } from 'drizzle-orm';
3
+ import { contentItems } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<{ removed: boolean }> => {
6
+ const db = useDB();
7
+ const user = requireAuth(event);
8
+ const { id, productId } = parseParams(event, { id: 'uuid', productId: 'uuid' });
9
+
10
+ // Ownership check
11
+ const [content] = await db
12
+ .select({ authorId: contentItems.authorId })
13
+ .from(contentItems)
14
+ .where(and(eq(contentItems.id, id), eq(contentItems.authorId, user.id)))
15
+ .limit(1);
16
+
17
+ if (!content) {
18
+ throw createError({ statusCode: 403, statusMessage: 'Not authorized to modify this content' });
19
+ }
20
+
21
+ const removed = await removeContentProduct(db, id, productId);
22
+
23
+ if (!removed) {
24
+ throw createError({ statusCode: 404, statusMessage: 'Product link not found' });
25
+ }
26
+
27
+ return { removed: true };
28
+ });
@@ -0,0 +1,29 @@
1
+ import { syncContentProducts } from '@commonpub/server';
2
+ import type { ContentProductItem } from '@commonpub/server';
3
+ import { eq, and } from 'drizzle-orm';
4
+ import { contentItems } from '@commonpub/schema';
5
+
6
+ export default defineEventHandler(async (event): Promise<ContentProductItem[]> => {
7
+ const db = useDB();
8
+ const user = requireAuth(event);
9
+ const { id } = parseParams(event, { id: 'uuid' });
10
+
11
+ // Ownership check
12
+ const [content] = await db
13
+ .select({ authorId: contentItems.authorId })
14
+ .from(contentItems)
15
+ .where(and(eq(contentItems.id, id), eq(contentItems.authorId, user.id)))
16
+ .limit(1);
17
+
18
+ if (!content) {
19
+ throw createError({ statusCode: 403, statusMessage: 'Not authorized to modify this content' });
20
+ }
21
+
22
+ const body = await readBody(event);
23
+
24
+ if (!Array.isArray(body?.items)) {
25
+ throw createError({ statusCode: 400, statusMessage: 'items array is required' });
26
+ }
27
+
28
+ return syncContentProducts(db, id, body.items);
29
+ });
@@ -0,0 +1,9 @@
1
+ import { listContentProducts } from '@commonpub/server';
2
+ import type { ContentProductItem } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<ContentProductItem[]> => {
5
+ const db = useDB();
6
+ const { id } = parseParams(event, { id: 'uuid' });
7
+
8
+ return listContentProducts(db, id);
9
+ });
@@ -0,0 +1,31 @@
1
+ import { addContentProduct } from '@commonpub/server';
2
+ import type { ContentProductItem } from '@commonpub/server';
3
+ import { addContentProductSchema } from '@commonpub/schema';
4
+ import { eq, and } from 'drizzle-orm';
5
+ import { contentItems } from '@commonpub/schema';
6
+
7
+ export default defineEventHandler(async (event): Promise<ContentProductItem> => {
8
+ const db = useDB();
9
+ const user = requireAuth(event);
10
+ const { id } = parseParams(event, { id: 'uuid' });
11
+ const input = await parseBody(event, addContentProductSchema);
12
+
13
+ // Ownership check
14
+ const [content] = await db
15
+ .select({ authorId: contentItems.authorId })
16
+ .from(contentItems)
17
+ .where(and(eq(contentItems.id, id), eq(contentItems.authorId, user.id)))
18
+ .limit(1);
19
+
20
+ if (!content) {
21
+ throw createError({ statusCode: 403, statusMessage: 'Not authorized to modify this content' });
22
+ }
23
+
24
+ const result = await addContentProduct(db, id, input);
25
+
26
+ if (!result) {
27
+ throw createError({ statusCode: 404, statusMessage: 'Product not found or already linked' });
28
+ }
29
+
30
+ return result;
31
+ });
@@ -0,0 +1,17 @@
1
+ import { publishContent, onContentPublished } from '@commonpub/server';
2
+ import type { ContentDetail } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<ContentDetail> => {
5
+ const user = requireAuth(event);
6
+ const db = useDB();
7
+ const config = useConfig();
8
+ const { id } = parseParams(event, { id: 'uuid' });
9
+
10
+ const content = await publishContent(db, id, user.id);
11
+ if (!content) {
12
+ throw createError({ statusCode: 404, statusMessage: 'Content not found or not owned by you' });
13
+ }
14
+
15
+ await onContentPublished(db, id, config);
16
+ return content;
17
+ });
@@ -0,0 +1,17 @@
1
+ import { createReport } from '@commonpub/server';
2
+ import { createReportSchema } from '@commonpub/schema';
3
+
4
+ export default defineEventHandler(async (event): Promise<{ id: string }> => {
5
+ const db = useDB();
6
+ const user = requireAuth(event);
7
+ const { id } = parseParams(event, { id: 'uuid' });
8
+
9
+ const body = await readBody(event);
10
+ const parsed = createReportSchema.safeParse({ ...body, targetId: id });
11
+
12
+ if (!parsed.success) {
13
+ throw createError({ statusCode: 400, statusMessage: 'Invalid input', data: parsed.error.flatten() });
14
+ }
15
+
16
+ return createReport(db, user.id, parsed.data);
17
+ });
@@ -0,0 +1,9 @@
1
+ import { listContentVersions } from '@commonpub/server';
2
+ import type { ContentVersionItem } from '@commonpub/server';
3
+
4
+ export default defineEventHandler(async (event): Promise<ContentVersionItem[]> => {
5
+ const db = useDB();
6
+ const { id } = parseParams(event, { id: 'uuid' });
7
+
8
+ return listContentVersions(db, id);
9
+ });
@@ -0,0 +1,34 @@
1
+ import { incrementViewCount } from '@commonpub/server';
2
+
3
+ // Simple in-memory dedup — tracks IP+contentId pairs for 5 minutes
4
+ const recentViews = new Map<string, number>();
5
+ const VIEW_COOLDOWN_MS = 5 * 60 * 1000;
6
+
7
+ // Periodic cleanup every 2 minutes
8
+ setInterval(() => {
9
+ const now = Date.now();
10
+ for (const [key, ts] of recentViews) {
11
+ if (now - ts > VIEW_COOLDOWN_MS) recentViews.delete(key);
12
+ }
13
+ }, 120_000);
14
+
15
+ export default defineEventHandler(async (event): Promise<{ success: boolean }> => {
16
+ const db = useDB();
17
+ const { id } = parseParams(event, { id: 'uuid' });
18
+
19
+ // De-duplicate views per IP + content within cooldown window
20
+ const ip = getRequestHeader(event, 'x-forwarded-for')?.split(',')[0]?.trim()
21
+ || getRequestHeader(event, 'x-real-ip')
22
+ || 'unknown';
23
+ const dedupKey = `${ip}:${id}`;
24
+ const lastView = recentViews.get(dedupKey);
25
+
26
+ if (lastView && Date.now() - lastView < VIEW_COOLDOWN_MS) {
27
+ // Already counted recently — skip but return success
28
+ return { success: true };
29
+ }
30
+
31
+ recentViews.set(dedupKey, Date.now());
32
+ await incrementViewCount(db, id);
33
+ return { success: true };
34
+ });
@@ -0,0 +1,23 @@
1
+ import { listContent } from '@commonpub/server';
2
+ import type { PaginatedResponse, ContentListItem } from '@commonpub/server';
3
+ import { contentFiltersSchema } from '@commonpub/schema';
4
+
5
+ export default defineEventHandler(async (event): Promise<PaginatedResponse<ContentListItem>> => {
6
+ const db = useDB();
7
+ const user = getOptionalUser(event);
8
+ const filters = parseQueryParams(event, contentFiltersSchema);
9
+
10
+ const isOwnContent = filters.authorId && user?.id === filters.authorId;
11
+
12
+ const config = useConfig();
13
+
14
+ return listContent(db, {
15
+ ...filters,
16
+ status: isOwnContent ? filters.status : (filters.status ?? 'published'),
17
+ // Only show public content unless viewing own content
18
+ visibility: isOwnContent ? filters.visibility : 'public',
19
+ }, {
20
+ includeFederated: config.features.seamlessFederation,
21
+ allowedContentTypes: config.instance.contentTypes,
22
+ });
23
+ });