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