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