studiocms 0.2.0 → 0.4.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 (293) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/cli/add/index.d.ts +2 -2
  3. package/dist/cli/add/index.js +4 -3
  4. package/dist/cli/add/npm-utils.d.ts +6 -6
  5. package/dist/cli/add/tryToInstallPlugins.d.ts +1 -1
  6. package/dist/cli/add/tryToInstallPlugins.js +6 -5
  7. package/dist/cli/add/updateStudioCMSConfig.d.ts +1 -1
  8. package/dist/cli/add/updateStudioCMSConfig.js +3 -4
  9. package/dist/cli/add/validatePlugins.d.ts +1 -2
  10. package/dist/cli/add/validatePlugins.js +15 -9
  11. package/dist/cli/crypto/genJWT/index.d.ts +1 -1
  12. package/dist/cli/crypto/genJWT/index.js +8 -9
  13. package/dist/cli/crypto/index.d.ts +1 -1
  14. package/dist/cli/init/steps/env.js +14 -4
  15. package/dist/cli/init/steps/next.d.ts +1 -1
  16. package/dist/cli/init/steps/next.js +6 -5
  17. package/dist/cli/migrator/index.d.ts +1 -1
  18. package/dist/cli/migrator/index.js +2 -2
  19. package/dist/cli/users/index.d.ts +1 -1
  20. package/dist/cli/users/shared.js +2 -2
  21. package/dist/cli/users/steps/createUsers.js +7 -7
  22. package/dist/cli/users/steps/modifyUsers.js +2 -2
  23. package/dist/cli/users/steps/next.d.ts +1 -1
  24. package/dist/cli/utils/checkRequiredEnvVars.js +2 -2
  25. package/dist/cli/utils/context.d.ts +2 -4
  26. package/dist/cli/utils/context.js +1 -3
  27. package/dist/cli/utils/getCliDbClient.d.ts +1 -1
  28. package/dist/cli/utils/intro.d.ts +1 -1
  29. package/dist/cli/utils/loadConfig.d.ts +54 -49
  30. package/dist/cli/utils/loadConfig.js +5 -8
  31. package/dist/cli/utils/logger.js +3 -3
  32. package/dist/cli/utils/user-utils.d.ts +1 -1
  33. package/dist/cli/utils/user-utils.js +4 -3
  34. package/dist/client/apiClient.d.ts +4923 -0
  35. package/dist/client/apiClient.js +72 -0
  36. package/dist/config.d.ts +1734 -1
  37. package/dist/consts.d.ts +5 -5
  38. package/dist/consts.js +3 -2
  39. package/dist/db/plugins.d.ts +1 -1
  40. package/dist/db/plugins.js +5 -8
  41. package/dist/handlers/frontend/routes.d.ts +4 -18
  42. package/dist/handlers/frontend/routes.js +13 -152
  43. package/dist/handlers/frontend/types.d.ts +1 -1
  44. package/dist/handlers/frontend/utils.js +0 -18
  45. package/dist/handlers/pluginHandler.d.ts +34 -257
  46. package/dist/handlers/pluginHandler.js +92 -46
  47. package/dist/handlers/routeHandler.js +32 -11
  48. package/dist/handlers/setupDbStudio.d.ts +3 -1
  49. package/dist/handlers/setupDbStudio.js +19 -10
  50. package/dist/handlers/storage-manager/core/effectify-astro-context.d.ts +25 -0
  51. package/dist/handlers/storage-manager/core/effectify-astro-context.js +78 -0
  52. package/dist/handlers/storage-manager/no-op.d.ts +2 -2
  53. package/dist/handlers/storage-manager/no-op.js +2 -3
  54. package/dist/index.d.ts +0 -1
  55. package/dist/index.js +10 -20
  56. package/dist/integrations/robots/index.d.ts +2 -2
  57. package/dist/integrations/robots/index.js +1 -3
  58. package/dist/integrations/robots/schema.d.ts +102 -273
  59. package/dist/integrations/robots/schema.js +220 -209
  60. package/dist/plugins/analytics/assets/schemas.d.ts +14 -9
  61. package/dist/plugins/analytics/assets/schemas.js +25 -17
  62. package/dist/plugins/analytics/db-client.d.ts +1 -1
  63. package/dist/plugins/analytics/index.d.ts +823 -3
  64. package/dist/plugins/analytics/index.js +4 -5
  65. package/dist/plugins/analytics/schemas.d.ts +54 -62
  66. package/dist/plugins/analytics/schemas.js +64 -13
  67. package/dist/plugins/analytics/table.d.ts +1 -1
  68. package/dist/plugins.d.ts +0 -1
  69. package/dist/schemas/config/api.d.ts +17 -0
  70. package/dist/schemas/config/api.js +14 -0
  71. package/dist/schemas/config/auth.d.ts +55 -59
  72. package/dist/schemas/config/auth.js +34 -11
  73. package/dist/schemas/config/dashboard.d.ts +43 -79
  74. package/dist/schemas/config/dashboard.js +43 -12
  75. package/dist/schemas/config/db.d.ts +15 -17
  76. package/dist/schemas/config/db.js +13 -5
  77. package/dist/schemas/config/developer.d.ts +33 -45
  78. package/dist/schemas/config/developer.js +22 -5
  79. package/dist/schemas/config/index.d.ts +398 -521
  80. package/dist/schemas/config/index.js +115 -57
  81. package/dist/schemas/config/sdk.d.ts +50 -196
  82. package/dist/schemas/config/sdk.js +61 -73
  83. package/dist/schemas/custom.d.ts +40 -0
  84. package/dist/schemas/custom.js +41 -0
  85. package/dist/schemas/external-schemas.d.ts +171 -0
  86. package/dist/schemas/external-schemas.js +179 -0
  87. package/dist/schemas/index.d.ts +2 -0
  88. package/dist/schemas/index.js +2 -0
  89. package/dist/schemas/plugins/i18n.d.ts +59 -39
  90. package/dist/schemas/plugins/i18n.js +42 -5
  91. package/dist/schemas/plugins/index.d.ts +7126 -10296
  92. package/dist/schemas/plugins/index.js +260 -276
  93. package/dist/schemas/plugins/shared.d.ts +1293 -3718
  94. package/dist/schemas/plugins/shared.js +320 -329
  95. package/dist/test-utils.d.ts +15 -4
  96. package/dist/test-utils.js +27 -11
  97. package/dist/toolbar/db-viewer/db-shared-types.d.ts +6 -6
  98. package/dist/toolbar/db-viewer/studio/connection.d.ts +8 -4
  99. package/dist/toolbar/db-viewer/studio/connection.js +2 -28
  100. package/dist/toolbar/db-viewer/studio/env/libsql.d.ts +7 -0
  101. package/dist/toolbar/db-viewer/studio/env/libsql.js +17 -0
  102. package/dist/toolbar/db-viewer/studio/env/mysql.d.ts +7 -0
  103. package/dist/toolbar/db-viewer/studio/env/mysql.js +23 -0
  104. package/dist/toolbar/db-viewer/studio/env/postgres.d.ts +7 -0
  105. package/dist/toolbar/db-viewer/studio/env/postgres.js +23 -0
  106. package/dist/toolbar/db-viewer/studio/index.js +20 -56
  107. package/dist/toolbar/db-viewer/studio/type.d.ts +1 -2
  108. package/dist/toolbar/db-viewer/studio/virtual-connection/libsql.d.ts +3 -0
  109. package/dist/toolbar/db-viewer/studio/virtual-connection/libsql.js +24 -0
  110. package/dist/toolbar/db-viewer/studio/virtual-connection/mysql.d.ts +3 -0
  111. package/dist/toolbar/db-viewer/studio/virtual-connection/mysql.js +9 -0
  112. package/dist/toolbar/db-viewer/studio/virtual-connection/postgres.d.ts +3 -0
  113. package/dist/toolbar/db-viewer/studio/virtual-connection/postgres.js +9 -0
  114. package/dist/toolbar/db-viewer/viewer.js +20 -21
  115. package/dist/types.d.ts +30 -0
  116. package/dist/utils/effects/smtp.d.ts +1 -1
  117. package/dist/utils/lang-helper.d.ts +10 -2
  118. package/dist/virtual.d.ts +35 -28
  119. package/dist/virtuals/auth/core.d.ts +5 -5
  120. package/dist/virtuals/auth/verify-email.d.ts +6 -6
  121. package/dist/virtuals/components/Generator.astro +2 -2
  122. package/dist/virtuals/components/Renderer.astro +9 -1
  123. package/dist/virtuals/components/renderFn.d.ts +3 -1
  124. package/dist/virtuals/components/renderFn.js +18 -0
  125. package/dist/virtuals/lib/headDefaults.d.ts +4 -2
  126. package/dist/virtuals/lib/headDefaults.js +0 -2
  127. package/dist/virtuals/lib/routeMap.d.ts +0 -12
  128. package/dist/virtuals/lib/routeMap.js +2 -14
  129. package/dist/virtuals/mailer/index.d.ts +3 -3
  130. package/dist/virtuals/notifier/index.d.ts +5 -5
  131. package/dist/virtuals/plugins/dashboard-pages.d.ts +2 -64
  132. package/dist/virtuals/scripts/StorageFileBrowser.d.ts +1 -172
  133. package/dist/virtuals/scripts/StorageFileBrowser.js +216 -119
  134. package/dist/virtuals/template-engine/index.d.ts +4 -4
  135. package/frontend/components/dashboard/configuration/ConfigForm.astro +218 -110
  136. package/frontend/components/dashboard/content-mgmt/ContentSearch.astro +21 -22
  137. package/frontend/components/dashboard/content-mgmt/CreateFolder.astro +66 -54
  138. package/frontend/components/dashboard/content-mgmt/CreatePage.astro +58 -104
  139. package/frontend/components/dashboard/content-mgmt/EditFolder.astro +65 -67
  140. package/frontend/components/dashboard/content-mgmt/EditPage.astro +86 -134
  141. package/frontend/components/dashboard/content-mgmt/InnerSidebarElement.astro +0 -1
  142. package/frontend/components/dashboard/content-mgmt/PageHeader.astro +33 -52
  143. package/frontend/components/dashboard/content-mgmt/PageTypeHandler.astro +2 -2
  144. package/frontend/components/dashboard/profile/APITokens.astro +219 -158
  145. package/frontend/components/dashboard/profile/BasicInfo.astro +165 -106
  146. package/frontend/components/dashboard/profile/Notifications.astro +27 -18
  147. package/frontend/components/dashboard/profile/UpdatePassword.astro +134 -94
  148. package/frontend/components/dashboard/sidebar/VersionCheck.astro +31 -16
  149. package/frontend/components/dashboard/sidebar/VersionCheckChangelog.astro +18 -11
  150. package/frontend/components/dashboard/sidebar-modals/VersionModal.astro +2 -2
  151. package/frontend/components/dashboard/smtp-config/TemplateEditor.astro +14 -14
  152. package/frontend/components/dashboard/taxonomy/InnerSidebarElement.astro +0 -1
  153. package/frontend/components/dashboard/taxonomy/MetaContainer.astro +0 -2
  154. package/frontend/components/dashboard/taxonomy/PageHeader.astro +16 -24
  155. package/frontend/components/dashboard/taxonomy/TaxonomySearch.astro +23 -27
  156. package/frontend/components/dashboard/user-mgmt/InnerSidebarElement.astro +111 -104
  157. package/frontend/components/dashboard/user-mgmt/UserListItem.astro +9 -22
  158. package/frontend/components/dashboard/user-mgmt/UserListItems.astro +18 -0
  159. package/frontend/components/first-time-setup/snippets/{opt2-studiocms.config.diff → studiocms.config.diff} +1 -0
  160. package/frontend/components/shared/Code.astro +1 -4
  161. package/frontend/components/shared/DynamicSettingsRenderer.astro +1 -1
  162. package/frontend/components/shared/SSRUser.astro +2 -4
  163. package/frontend/components/shared/foldertree/FolderTreeNode.astro +0 -6
  164. package/frontend/components/shared/storage-manager/StorageCopyOutput.astro +0 -1
  165. package/frontend/components/shared/taxonomy/TaxonomyTreeNode.astro +0 -6
  166. package/frontend/layouts/DashboardLayout.astro +1 -10
  167. package/frontend/layouts/TaxonomyLayout.astro +0 -1
  168. package/frontend/middleware/index.ts +102 -61
  169. package/frontend/pages/404.astro +5 -9
  170. package/frontend/pages/[dashboard]/[...pluginPage].astro +10 -1
  171. package/frontend/pages/[dashboard]/configuration.astro +10 -1
  172. package/frontend/pages/[dashboard]/content-management/createfolder.astro +10 -1
  173. package/frontend/pages/[dashboard]/content-management/createpage.astro +10 -1
  174. package/frontend/pages/[dashboard]/content-management/diff.astro +39 -14
  175. package/frontend/pages/[dashboard]/content-management/editfolder.astro +10 -1
  176. package/frontend/pages/[dashboard]/content-management/editpage.astro +10 -1
  177. package/frontend/pages/[dashboard]/content-management/index.astro +10 -1
  178. package/frontend/pages/[dashboard]/index.astro +10 -1
  179. package/frontend/pages/[dashboard]/login.astro +86 -25
  180. package/frontend/pages/[dashboard]/password-reset.astro +22 -16
  181. package/frontend/pages/[dashboard]/plugins/[plugin].astro +10 -1
  182. package/frontend/pages/[dashboard]/profile.astro +10 -1
  183. package/frontend/pages/[dashboard]/signup.astro +153 -52
  184. package/frontend/pages/[dashboard]/smtp-configuration.astro +77 -75
  185. package/frontend/pages/[dashboard]/system-management.astro +10 -1
  186. package/frontend/pages/[dashboard]/taxonomy/categories.astro +30 -41
  187. package/frontend/pages/[dashboard]/taxonomy/index.astro +10 -0
  188. package/frontend/pages/[dashboard]/taxonomy/tags.astro +33 -43
  189. package/frontend/pages/[dashboard]/unverified-email.astro +29 -21
  190. package/frontend/pages/[dashboard]/user-management/edit.astro +170 -90
  191. package/frontend/pages/[dashboard]/user-management/index.astro +10 -1
  192. package/frontend/pages/studiocms_api/[...all].ts +106 -0
  193. package/frontend/pages/studiocms_api/_handlers/_utils/auth.ts +26 -0
  194. package/frontend/pages/studiocms_api/_handlers/_utils/changelog.ts +147 -0
  195. package/frontend/pages/studiocms_api/_handlers/_utils/db-studio-driver.ts +46 -0
  196. package/frontend/pages/studiocms_api/_handlers/_utils/parseLogLevel.ts +27 -0
  197. package/frontend/pages/studiocms_api/_handlers/auth/auth.ts +459 -0
  198. package/frontend/pages/studiocms_api/_handlers/auth/index.ts +17 -0
  199. package/frontend/pages/studiocms_api/_handlers/auth/oauth.ts +91 -0
  200. package/frontend/pages/studiocms_api/_handlers/dashboard/_shared.ts +57 -0
  201. package/frontend/pages/studiocms_api/_handlers/dashboard/apiTokens.ts +134 -0
  202. package/frontend/pages/studiocms_api/_handlers/dashboard/config.ts +64 -0
  203. package/frontend/pages/studiocms_api/_handlers/dashboard/content.ts +741 -0
  204. package/frontend/pages/studiocms_api/_handlers/dashboard/create.ts +480 -0
  205. package/frontend/pages/studiocms_api/_handlers/dashboard/emailNotifications.ts +49 -0
  206. package/frontend/pages/studiocms_api/_handlers/dashboard/index.ts +45 -0
  207. package/frontend/pages/studiocms_api/_handlers/dashboard/mailer.ts +136 -0
  208. package/frontend/pages/studiocms_api/_handlers/dashboard/plugins.ts +80 -0
  209. package/frontend/pages/studiocms_api/_handlers/dashboard/profile.ts +275 -0
  210. package/frontend/pages/studiocms_api/_handlers/dashboard/resetPassword.ts +140 -0
  211. package/frontend/pages/studiocms_api/_handlers/dashboard/search.ts +63 -0
  212. package/frontend/pages/studiocms_api/_handlers/dashboard/taxonomy.ts +285 -0
  213. package/frontend/pages/studiocms_api/_handlers/dashboard/templates.ts +75 -0
  214. package/frontend/pages/studiocms_api/_handlers/dashboard/users.ts +312 -0
  215. package/frontend/pages/studiocms_api/_handlers/dashboard/verifyEndpoints.ts +307 -0
  216. package/frontend/pages/studiocms_api/_handlers/integration/dbStudio.ts +98 -0
  217. package/frontend/pages/studiocms_api/_handlers/integration/index.ts +17 -0
  218. package/frontend/pages/studiocms_api/_handlers/integration/storageManager.ts +107 -0
  219. package/frontend/pages/studiocms_api/_handlers/rest-api/index.ts +8 -0
  220. package/frontend/pages/studiocms_api/_handlers/rest-api/v1/_shared.ts +41 -0
  221. package/frontend/pages/studiocms_api/_handlers/rest-api/v1/index.ts +17 -0
  222. package/frontend/pages/studiocms_api/_handlers/rest-api/v1/public.ts +195 -0
  223. package/frontend/pages/studiocms_api/_handlers/rest-api/v1/secure.ts +1726 -0
  224. package/frontend/pages/studiocms_api/_handlers/sdk.ts +129 -0
  225. package/frontend/pages/studiocms_api/_middleware/astroLocals.ts +36 -0
  226. package/frontend/pages/studiocms_api/_middleware/restApi.ts +56 -0
  227. package/frontend/pages/studiocms_api/integrations/[...all].ts +8 -0
  228. package/frontend/scripts/formdata.ts +219 -0
  229. package/frontend/setup-pages/3-done.astro +4 -20
  230. package/frontend/setup-pages/studiocms_api/dashboard/step-2.ts +3 -6
  231. package/frontend/styles/dashboard-base.css +0 -1
  232. package/frontend/web-vitals/endpoint.ts +2 -1
  233. package/package.json +35 -31
  234. package/dist/global.d.ts +0 -9
  235. package/frontend/components/dashboard/LoginChecker.astro +0 -68
  236. package/frontend/components/dashboard/user-mgmt/RankCheck.astro +0 -57
  237. package/frontend/components/first-time-setup/snippets/opt1-astro.config.diff +0 -14
  238. package/frontend/components/first-time-setup/snippets/opt2-astro.config.diff +0 -9
  239. package/frontend/middleware/_authmap.ts +0 -63
  240. package/frontend/pages/studiocms_api/auth/[path].ts +0 -390
  241. package/frontend/pages/studiocms_api/auth/[provider]/[...id].ts +0 -64
  242. package/frontend/pages/studiocms_api/auth/[provider]/_effects/index.ts +0 -101
  243. package/frontend/pages/studiocms_api/auth/_shared.ts +0 -52
  244. package/frontend/pages/studiocms_api/dashboard/api-tokens.ts +0 -115
  245. package/frontend/pages/studiocms_api/dashboard/config.ts +0 -74
  246. package/frontend/pages/studiocms_api/dashboard/content/diff.ts +0 -73
  247. package/frontend/pages/studiocms_api/dashboard/content/folder.ts +0 -220
  248. package/frontend/pages/studiocms_api/dashboard/content/page.ts +0 -359
  249. package/frontend/pages/studiocms_api/dashboard/create-reset-link.ts +0 -77
  250. package/frontend/pages/studiocms_api/dashboard/create-user-invite.ts +0 -231
  251. package/frontend/pages/studiocms_api/dashboard/create-user.ts +0 -186
  252. package/frontend/pages/studiocms_api/dashboard/email-notification-settings-site.ts +0 -74
  253. package/frontend/pages/studiocms_api/dashboard/mailer/check-email.ts +0 -75
  254. package/frontend/pages/studiocms_api/dashboard/mailer/config.ts +0 -136
  255. package/frontend/pages/studiocms_api/dashboard/plugins/[plugin].ts +0 -80
  256. package/frontend/pages/studiocms_api/dashboard/profile.ts +0 -245
  257. package/frontend/pages/studiocms_api/dashboard/resend-verify-email.ts +0 -77
  258. package/frontend/pages/studiocms_api/dashboard/reset-password.ts +0 -124
  259. package/frontend/pages/studiocms_api/dashboard/search-list.ts +0 -59
  260. package/frontend/pages/studiocms_api/dashboard/taxonomy-search.ts +0 -47
  261. package/frontend/pages/studiocms_api/dashboard/taxonomy.ts +0 -230
  262. package/frontend/pages/studiocms_api/dashboard/templates.ts +0 -74
  263. package/frontend/pages/studiocms_api/dashboard/update-user-notifications.ts +0 -86
  264. package/frontend/pages/studiocms_api/dashboard/users.ts +0 -236
  265. package/frontend/pages/studiocms_api/dashboard/verify-email.ts +0 -83
  266. package/frontend/pages/studiocms_api/dashboard/verify-session.ts +0 -187
  267. package/frontend/pages/studiocms_api/integrations/[type]/[...id].ts +0 -15
  268. package/frontend/pages/studiocms_api/integrations/[type]/_routes/db-studio.ts +0 -173
  269. package/frontend/pages/studiocms_api/integrations/[type]/_routes/storage.ts +0 -88
  270. package/frontend/pages/studiocms_api/partials/editor.astro +0 -74
  271. package/frontend/pages/studiocms_api/partials/render.astro +0 -39
  272. package/frontend/pages/studiocms_api/partials/user-list-items.astro +0 -43
  273. package/frontend/pages/studiocms_api/rest/utils/auth-token.ts +0 -59
  274. package/frontend/pages/studiocms_api/rest/v1/[type]/[...id].ts +0 -23
  275. package/frontend/pages/studiocms_api/rest/v1/[type]/_routes/categories.ts +0 -267
  276. package/frontend/pages/studiocms_api/rest/v1/[type]/_routes/folders.ts +0 -283
  277. package/frontend/pages/studiocms_api/rest/v1/[type]/_routes/pages.ts +0 -505
  278. package/frontend/pages/studiocms_api/rest/v1/[type]/_routes/settings.ts +0 -100
  279. package/frontend/pages/studiocms_api/rest/v1/[type]/_routes/tags.ts +0 -229
  280. package/frontend/pages/studiocms_api/rest/v1/[type]/_routes/users.ts +0 -553
  281. package/frontend/pages/studiocms_api/rest/v1/public/[type]/[...id].ts +0 -19
  282. package/frontend/pages/studiocms_api/rest/v1/public/[type]/_routes/categories.ts +0 -74
  283. package/frontend/pages/studiocms_api/rest/v1/public/[type]/_routes/folders.ts +0 -85
  284. package/frontend/pages/studiocms_api/rest/v1/public/[type]/_routes/pages.ts +0 -103
  285. package/frontend/pages/studiocms_api/rest/v1/public/[type]/_routes/tags.ts +0 -67
  286. package/frontend/pages/studiocms_api/sdk/[...path].ts +0 -97
  287. package/frontend/pages/studiocms_api/sdk/utils/changelog.ts +0 -119
  288. package/frontend/scripts/auth/formListener.ts +0 -81
  289. package/frontend/scripts/formdata-utils.ts +0 -116
  290. package/frontend/utils/build-partial-schema.ts +0 -46
  291. package/frontend/utils/errors.ts +0 -6
  292. package/frontend/utils/param-extractor.ts +0 -83
  293. package/frontend/utils/rest-router.ts +0 -444
@@ -0,0 +1,1726 @@
1
+ import { Password, User } from 'studiocms:auth/lib';
2
+ import { Notifications } from 'studiocms:notifier';
3
+ import { SDKCore } from 'studiocms:sdk';
4
+ import type { tsPageContentSelect, tsPageData, tsPageDataSelect } from 'studiocms:sdk/types';
5
+ import routeConfig from 'virtual:studiocms/route-config';
6
+ import { HttpApiBuilder } from '@effect/platform';
7
+ import { StudioCMSRestApiV1Spec } from '@withstudiocms/api-spec';
8
+ import {
9
+ APISafeUserFields,
10
+ CurrentRestAPIUser,
11
+ RestAPIError,
12
+ } from '@withstudiocms/api-spec/rest-api';
13
+ import {
14
+ type AvailablePermissionRanks,
15
+ availablePermissionRanks,
16
+ } from '@withstudiocms/auth-kit/types';
17
+ import {
18
+ StudioCMSPageData,
19
+ StudioCMSPageDataCategories,
20
+ StudioCMSPageDataTags,
21
+ StudioCMSPageFolderStructure,
22
+ } from '@withstudiocms/sdk/tables';
23
+ import { Effect, Layer, Schema } from 'effect';
24
+ import { isValidEmail } from '#schemas';
25
+ import { RestAPIAuthorizationLive } from '../../../_middleware/restApi.js';
26
+ import { sharedDBErrors, sharedNotifierErrors, sharedPageCollectionErrors } from './_shared.js';
27
+
28
+ const restAPIEnabled = routeConfig.restAPIEnabled;
29
+
30
+ /**
31
+ * Utility schema for encoding Arrays of Strings
32
+ */
33
+ const StringArrayCodec = Schema.transform(Schema.String, Schema.Array(Schema.String), {
34
+ strict: true,
35
+ decode: (data) => JSON.parse(data),
36
+ encode: (data) => JSON.stringify(data),
37
+ });
38
+
39
+ /**
40
+ * Function that uses the String Array Schema to create a JSON.stringify like function for string arrays
41
+ */
42
+ const encodeStringArray = Schema.encode(StringArrayCodec);
43
+
44
+ /**
45
+ * REST API v1 Secure Handler
46
+ *
47
+ * This handler is responsible for managing all secure endpoints of the REST API v1. It includes endpoints for creating, updating, deleting, and retrieving categories, folders, pages, settings, tags, and users. Each endpoint is currently implemented as a placeholder that returns an empty Effect, but can be expanded in the future to include actual logic for interacting with the database and performing the necessary operations.
48
+ */
49
+ export const RestApiSecureHandler = HttpApiBuilder.group(
50
+ StudioCMSRestApiV1Spec,
51
+ 'restV1',
52
+ (handlers) =>
53
+ handlers
54
+ // Category Endpoints
55
+ .handle(
56
+ 'createCategory',
57
+ Effect.fn(
58
+ function* ({ payload }) {
59
+ if (!restAPIEnabled) {
60
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
61
+ }
62
+ const [sdk, user, notifier] = yield* Effect.all([
63
+ SDKCore,
64
+ CurrentRestAPIUser,
65
+ Notifications,
66
+ ]);
67
+
68
+ const { rank } = user;
69
+
70
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
71
+ return yield* new RestAPIError({ error: 'Unauthorized' });
72
+ }
73
+
74
+ const newId = yield* sdk.UTIL.Generators.generateRandomIDNumber(9);
75
+ const newCategory = { id: newId, ...payload };
76
+
77
+ yield* sdk.POST.databaseEntry.categories(newCategory);
78
+ yield* notifier
79
+ .sendEditorNotification('new_category', newCategory.name)
80
+ .pipe(
81
+ Effect.catchAll(
82
+ () => new RestAPIError({ error: 'Failed to send notification for new category' })
83
+ )
84
+ );
85
+
86
+ return yield* sdk.GET.categories
87
+ .byId(newId)
88
+ .pipe(
89
+ Effect.flatMap((category) =>
90
+ category
91
+ ? Effect.succeed(category)
92
+ : Effect.fail(
93
+ new RestAPIError({ error: 'Failed to retrieve newly created category' })
94
+ )
95
+ )
96
+ );
97
+ },
98
+ Notifications.Provide,
99
+ Effect.catchTags({
100
+ ...sharedDBErrors,
101
+ ...sharedNotifierErrors,
102
+ GeneratorError: () =>
103
+ new RestAPIError({ error: 'Failed to generate new ID for category' }),
104
+ })
105
+ )
106
+ )
107
+ .handle(
108
+ 'deleteCategory',
109
+ Effect.fn(
110
+ function* ({ path: { id } }) {
111
+ if (!restAPIEnabled) {
112
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
113
+ }
114
+ const [sdk, user, notifier] = yield* Effect.all([
115
+ SDKCore,
116
+ CurrentRestAPIUser,
117
+ Notifications,
118
+ ]);
119
+
120
+ const checkForChildrenCategories = sdk.dbService.withCodec({
121
+ encoder: Schema.Number,
122
+ decoder: Schema.Array(StudioCMSPageDataCategories.Select),
123
+ callbackFn: (client, categoryId) =>
124
+ client((db) =>
125
+ db
126
+ .selectFrom('StudioCMSPageDataCategories')
127
+ .where('parent', '=', categoryId)
128
+ .selectAll()
129
+ .execute()
130
+ ),
131
+ });
132
+
133
+ const getPageList = sdk.GET.pages(true, true);
134
+
135
+ const flattenAndCount = <T extends { id: number }>(arrays: T[][]): boolean => {
136
+ return arrays.flat().filter((data) => data.id === id).length > 0;
137
+ };
138
+
139
+ const checkForChildrenPagesCategories = () =>
140
+ getPageList.pipe(
141
+ Effect.map((data) => data.map(({ categories }) => categories)),
142
+ Effect.map(flattenAndCount)
143
+ );
144
+
145
+ const { rank } = user;
146
+
147
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
148
+ return yield* new RestAPIError({ error: 'Unauthorized' });
149
+ }
150
+
151
+ const existingCategory = yield* sdk.GET.categories.byId(id);
152
+
153
+ if (!existingCategory) {
154
+ return yield* new RestAPIError({ error: 'Category not found' });
155
+ }
156
+
157
+ const hasChildrenCategories = yield* checkForChildrenCategories(id).pipe(
158
+ Effect.map((categories) => categories.length > 0)
159
+ );
160
+
161
+ if (hasChildrenCategories) {
162
+ return yield* new RestAPIError({
163
+ error: 'Cannot delete category with child categories',
164
+ });
165
+ }
166
+
167
+ const hasChildrenPages = yield* checkForChildrenPagesCategories();
168
+
169
+ if (hasChildrenPages) {
170
+ return yield* new RestAPIError({ error: 'Cannot delete category assigned to pages' });
171
+ }
172
+
173
+ yield* sdk.DELETE.categories(id);
174
+ yield* notifier
175
+ .sendEditorNotification('delete_category', existingCategory.name)
176
+ .pipe(
177
+ Effect.catchAll(
178
+ () =>
179
+ new RestAPIError({ error: 'Failed to send notification for category deletion' })
180
+ )
181
+ );
182
+
183
+ return { success: true };
184
+ },
185
+ Notifications.Provide,
186
+ Effect.catchTags({
187
+ ...sharedDBErrors,
188
+ ...sharedNotifierErrors,
189
+ ...sharedPageCollectionErrors,
190
+ })
191
+ )
192
+ )
193
+ .handle(
194
+ 'updateCategory',
195
+ Effect.fn(
196
+ function* ({ path: { id }, payload }) {
197
+ if (!restAPIEnabled) {
198
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
199
+ }
200
+ const [sdk, user, notifier] = yield* Effect.all([
201
+ SDKCore,
202
+ CurrentRestAPIUser,
203
+ Notifications,
204
+ ]);
205
+
206
+ const updateCategory = sdk.dbService.withCodec({
207
+ encoder: Schema.partial(StudioCMSPageDataCategories.Select),
208
+ decoder: StudioCMSPageDataCategories.Select,
209
+ callbackFn: (db, data) =>
210
+ db((client) =>
211
+ client.transaction().execute(async (trx) => {
212
+ await trx
213
+ .updateTable('StudioCMSPageDataCategories')
214
+ .set(data)
215
+ .where('id', '=', id)
216
+ .executeTakeFirstOrThrow();
217
+
218
+ return await trx
219
+ .selectFrom('StudioCMSPageDataCategories')
220
+ .selectAll()
221
+ .where('id', '=', id)
222
+ .executeTakeFirstOrThrow();
223
+ })
224
+ ),
225
+ });
226
+
227
+ const { rank } = user;
228
+
229
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
230
+ return yield* new RestAPIError({ error: 'Unauthorized' });
231
+ }
232
+
233
+ if (payload.id && payload.id !== id) {
234
+ return yield* new RestAPIError({
235
+ error: "ID in payload does not match ID in path, ID's must match to update.",
236
+ });
237
+ }
238
+
239
+ const parentId = payload.parent;
240
+ if (parentId && parentId === id) {
241
+ return yield* new RestAPIError({
242
+ error: 'Category cannot be its own parent',
243
+ });
244
+ }
245
+
246
+ const updatedCategory = yield* updateCategory(payload);
247
+
248
+ yield* notifier
249
+ .sendEditorNotification('update_category', updatedCategory.name)
250
+ .pipe(
251
+ Effect.catchAll(
252
+ () =>
253
+ new RestAPIError({ error: 'Failed to send notification for category update' })
254
+ )
255
+ );
256
+
257
+ return updatedCategory;
258
+ },
259
+ Notifications.Provide,
260
+ Effect.catchTags({
261
+ ...sharedDBErrors,
262
+ ...sharedNotifierErrors,
263
+ })
264
+ )
265
+ )
266
+ .handle(
267
+ 'getCategories',
268
+ Effect.fn(function* ({ urlParams: { name, parent } }) {
269
+ if (!restAPIEnabled) {
270
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
271
+ }
272
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
273
+
274
+ const { rank } = user;
275
+
276
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
277
+ return yield* new RestAPIError({ error: 'Unauthorized' });
278
+ }
279
+
280
+ let categories = yield* sdk.GET.categories.getAll();
281
+
282
+ if (name) {
283
+ categories = categories.filter((category) => category.name.includes(name));
284
+ }
285
+ if (parent) {
286
+ categories = categories.filter((category) => category.parent === parent);
287
+ }
288
+
289
+ return categories;
290
+ }, Effect.catchTags(sharedDBErrors))
291
+ )
292
+ .handle(
293
+ 'getCategory',
294
+ Effect.fn(function* ({ path: { id } }) {
295
+ if (!restAPIEnabled) {
296
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
297
+ }
298
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
299
+
300
+ const { rank } = user;
301
+
302
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
303
+ return yield* new RestAPIError({ error: 'Unauthorized' });
304
+ }
305
+
306
+ return yield* sdk.GET.categories
307
+ .byId(id)
308
+ .pipe(
309
+ Effect.flatMap((category) =>
310
+ category
311
+ ? Effect.succeed(category)
312
+ : Effect.fail(new RestAPIError({ error: 'Category not found' }))
313
+ )
314
+ );
315
+ }, Effect.catchTags(sharedDBErrors))
316
+ )
317
+
318
+ // Folder Endpoints
319
+ .handle(
320
+ 'createFolder',
321
+ Effect.fn(
322
+ function* ({ payload }) {
323
+ if (!restAPIEnabled) {
324
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
325
+ }
326
+ const [sdk, user, notifier] = yield* Effect.all([
327
+ SDKCore,
328
+ CurrentRestAPIUser,
329
+ Notifications,
330
+ ]);
331
+
332
+ const { rank } = user;
333
+
334
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
335
+ return yield* new RestAPIError({ error: 'Unauthorized' });
336
+ }
337
+
338
+ const newFolderData = { ...payload, id: crypto.randomUUID() };
339
+
340
+ yield* sdk.POST.databaseEntry.folder(newFolderData);
341
+
342
+ yield* notifier
343
+ .sendEditorNotification('new_folder', newFolderData.name)
344
+ .pipe(
345
+ Effect.catchAll(
346
+ () => new RestAPIError({ error: 'Failed to send notification for new folder' })
347
+ )
348
+ );
349
+
350
+ yield* Effect.all([sdk.UPDATE.folderList, sdk.UPDATE.folderTree]);
351
+
352
+ return {
353
+ message: `Folder created successfully with id: ${newFolderData.id}`,
354
+ };
355
+ },
356
+ Notifications.Provide,
357
+ Effect.catchTags({
358
+ ...sharedDBErrors,
359
+ ...sharedNotifierErrors,
360
+ FolderTreeError: () =>
361
+ new RestAPIError({ error: 'Failed to update folder tree after creating folder' }),
362
+ })
363
+ )
364
+ )
365
+ .handle(
366
+ 'deleteFolder',
367
+ Effect.fn(
368
+ function* ({ path: { id } }) {
369
+ if (!restAPIEnabled) {
370
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
371
+ }
372
+ const [sdk, user, notifier] = yield* Effect.all([
373
+ SDKCore,
374
+ CurrentRestAPIUser,
375
+ Notifications,
376
+ ]);
377
+
378
+ /**
379
+ * Check for child folders before deletion
380
+ */
381
+ const checkForChildrenFolders = sdk.dbService.withCodec({
382
+ encoder: Schema.String,
383
+ decoder: Schema.Array(StudioCMSPageFolderStructure),
384
+ callbackFn: (client, id) =>
385
+ client((db) =>
386
+ db
387
+ .selectFrom('StudioCMSPageFolderStructure')
388
+ .where('parent', '=', id)
389
+ .selectAll()
390
+ .execute()
391
+ ),
392
+ });
393
+
394
+ /**
395
+ * Check for child pages before deletion
396
+ */
397
+ const checkForChildrenPages = sdk.dbService.withCodec({
398
+ encoder: Schema.String,
399
+ decoder: Schema.Array(StudioCMSPageData),
400
+ callbackFn: (client, id) =>
401
+ client((db) =>
402
+ db
403
+ .selectFrom('StudioCMSPageData')
404
+ .where('parentFolder', '=', id)
405
+ .selectAll()
406
+ .execute()
407
+ ),
408
+ });
409
+
410
+ /**
411
+ * Check for any children (folders or pages) before deletion
412
+ */
413
+ const checkForChildren = Effect.fn((id: string) =>
414
+ Effect.all({
415
+ folders: checkForChildrenFolders(id),
416
+ pages: checkForChildrenPages(id),
417
+ }).pipe(
418
+ Effect.map(({ folders, pages }) => {
419
+ return { hasChildren: folders.length > 0 || pages.length > 0 };
420
+ })
421
+ )
422
+ );
423
+
424
+ const { rank } = user;
425
+
426
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
427
+ return yield* new RestAPIError({ error: 'Unauthorized' });
428
+ }
429
+
430
+ const existingFolder = yield* sdk.GET.folder(id);
431
+
432
+ if (!existingFolder) {
433
+ return yield* new RestAPIError({ error: 'Folder not found' });
434
+ }
435
+
436
+ const { hasChildren } = yield* checkForChildren(id);
437
+
438
+ if (hasChildren) {
439
+ return yield* new RestAPIError({
440
+ error: 'Cannot delete folder with child folders or pages',
441
+ });
442
+ }
443
+
444
+ yield* Effect.all([
445
+ sdk.DELETE.folder(id),
446
+ sdk.UPDATE.folderList,
447
+ sdk.UPDATE.folderTree,
448
+ ]);
449
+
450
+ yield* notifier
451
+ .sendEditorNotification('folder_deleted', existingFolder.name)
452
+ .pipe(
453
+ Effect.catchAll(
454
+ () =>
455
+ new RestAPIError({ error: 'Failed to send notification for folder deletion' })
456
+ )
457
+ );
458
+
459
+ return {
460
+ success: true,
461
+ };
462
+ },
463
+ Notifications.Provide,
464
+ Effect.catchTags({
465
+ ...sharedDBErrors,
466
+ ...sharedNotifierErrors,
467
+ FolderTreeError: () =>
468
+ new RestAPIError({ error: 'Failed to update folder tree after deleting folder' }),
469
+ })
470
+ )
471
+ )
472
+ .handle(
473
+ 'updateFolder',
474
+ Effect.fn(
475
+ function* ({ path: { id }, payload }) {
476
+ if (!restAPIEnabled) {
477
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
478
+ }
479
+ const [sdk, user, notifier] = yield* Effect.all([
480
+ SDKCore,
481
+ CurrentRestAPIUser,
482
+ Notifications,
483
+ ]);
484
+
485
+ const { rank } = user;
486
+
487
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
488
+ return yield* new RestAPIError({ error: 'Unauthorized' });
489
+ }
490
+
491
+ const { folderName, parentFolder } = payload;
492
+
493
+ if (parentFolder && parentFolder === id) {
494
+ return yield* new RestAPIError({ error: 'Folder cannot be its own parent' });
495
+ }
496
+
497
+ const existingFolder = yield* sdk.GET.folder(id);
498
+
499
+ if (!existingFolder) {
500
+ return yield* new RestAPIError({ error: 'Folder not found' });
501
+ }
502
+
503
+ const folderData = yield* sdk.UPDATE.folder({
504
+ id,
505
+ name: folderName,
506
+ parent: parentFolder || null,
507
+ });
508
+
509
+ yield* notifier
510
+ .sendEditorNotification('folder_updated', folderData.name)
511
+ .pipe(
512
+ Effect.catchAll(
513
+ () => new RestAPIError({ error: 'Failed to send notification for folder update' })
514
+ )
515
+ );
516
+
517
+ yield* Effect.all([sdk.UPDATE.folderList, sdk.UPDATE.folderTree]);
518
+
519
+ return {
520
+ message: 'Folder updated successfully',
521
+ };
522
+ },
523
+ Notifications.Provide,
524
+ Effect.catchTags({
525
+ ...sharedDBErrors,
526
+ ...sharedNotifierErrors,
527
+ FolderTreeError: () =>
528
+ new RestAPIError({ error: 'Failed to update folder tree after creating folder' }),
529
+ })
530
+ )
531
+ )
532
+ .handle(
533
+ 'getFolders',
534
+ Effect.fn(function* ({ urlParams: { name, parent } }) {
535
+ if (!restAPIEnabled) {
536
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
537
+ }
538
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
539
+
540
+ const { rank } = user;
541
+
542
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
543
+ return yield* new RestAPIError({ error: 'Unauthorized' });
544
+ }
545
+
546
+ let folders = yield* sdk.GET.folderList();
547
+
548
+ if (name) {
549
+ folders = folders.filter((folder) => folder.name.includes(name));
550
+ }
551
+ if (parent) {
552
+ folders = folders.filter((folder) => folder.parent === parent);
553
+ }
554
+
555
+ return folders;
556
+ }, Effect.catchTags(sharedDBErrors))
557
+ )
558
+ .handle(
559
+ 'getFolder',
560
+ Effect.fn(function* ({ path: { id } }) {
561
+ if (!restAPIEnabled) {
562
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
563
+ }
564
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
565
+
566
+ if (user.rank !== 'owner' && user.rank !== 'admin' && user.rank !== 'editor') {
567
+ return yield* new RestAPIError({ error: 'Unauthorized' });
568
+ }
569
+
570
+ return yield* sdk.GET.folder(id).pipe(
571
+ Effect.flatMap((folder) =>
572
+ folder
573
+ ? Effect.succeed(folder)
574
+ : Effect.fail(new RestAPIError({ error: 'Folder not found' }))
575
+ )
576
+ );
577
+ }, Effect.catchTags(sharedDBErrors))
578
+ )
579
+
580
+ // Page Endpoints
581
+ .handle(
582
+ 'createPage',
583
+ Effect.fn(
584
+ function* ({ payload: { data, content } }) {
585
+ if (!restAPIEnabled) {
586
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
587
+ }
588
+ const [sdk, user, notifier] = yield* Effect.all([
589
+ SDKCore,
590
+ CurrentRestAPIUser,
591
+ Notifications,
592
+ ]);
593
+
594
+ const { rank, userId } = user;
595
+
596
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
597
+ return yield* new RestAPIError({ error: 'Unauthorized' });
598
+ }
599
+
600
+ if (!data) {
601
+ return yield* new RestAPIError({ error: 'Page data is required' });
602
+ }
603
+
604
+ if (!content) {
605
+ return yield* new RestAPIError({ error: 'Page content is required' });
606
+ }
607
+
608
+ if (!data.title) {
609
+ return yield* new RestAPIError({ error: 'Page title is required in page data' });
610
+ }
611
+
612
+ const dataId = crypto.randomUUID();
613
+
614
+ const {
615
+ title,
616
+ slug,
617
+ description,
618
+ categories,
619
+ tags,
620
+ contributorIds,
621
+ augments,
622
+ id: ___id,
623
+ authorId: __authorId,
624
+ updatedAt: __updatedAt,
625
+ publishedAt: __publishedAt,
626
+ ...restPageData
627
+ } = data;
628
+
629
+ const { id: ____id, ...contentData } = content;
630
+
631
+ yield* sdk.POST.databaseEntry.pages(
632
+ {
633
+ id: dataId,
634
+ title,
635
+ slug:
636
+ slug ||
637
+ title
638
+ .toLowerCase()
639
+ .replace(/[^a-z0-9\s-]/g, '') // Remove special characters
640
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
641
+ .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
642
+ .replace(/^-|-$/g, ''), // Remove leading/trailing hyphens '-'),
643
+ description: description || '',
644
+ authorId: userId,
645
+ updatedAt: new Date().toISOString(),
646
+ publishedAt: new Date().toISOString(),
647
+ categories: yield* encodeStringArray(categories || []),
648
+ tags: yield* encodeStringArray(tags || []),
649
+ contributorIds: yield* encodeStringArray(contributorIds || []),
650
+ augments: yield* encodeStringArray(augments || []),
651
+ ...(restPageData as Omit<
652
+ tsPageData['Insert']['Type'],
653
+ | 'id'
654
+ | 'title'
655
+ | 'slug'
656
+ | 'description'
657
+ | 'authorId'
658
+ | 'updatedAt'
659
+ | 'publishedAt'
660
+ | 'categories'
661
+ | 'tags'
662
+ | 'contributorIds'
663
+ | 'augments'
664
+ >),
665
+ },
666
+ { ...contentData } as tsPageContentSelect
667
+ );
668
+
669
+ yield* notifier
670
+ .sendEditorNotification('new_page', data.title)
671
+ .pipe(
672
+ Effect.catchAll(
673
+ () => new RestAPIError({ error: 'Failed to send notification for new page' })
674
+ )
675
+ );
676
+
677
+ return {
678
+ message: `Page created successfully with id: ${dataId}`,
679
+ };
680
+ },
681
+ Notifications.Provide,
682
+ Effect.catchTags({
683
+ ...sharedDBErrors,
684
+ ...sharedNotifierErrors,
685
+ ParseError: () =>
686
+ new RestAPIError({ error: 'Failed to parse page data during creation' }),
687
+ })
688
+ )
689
+ )
690
+ .handle(
691
+ 'deletePage',
692
+ Effect.fn(
693
+ function* ({ path: { id }, payload: { slug } }) {
694
+ if (!restAPIEnabled) {
695
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
696
+ }
697
+ const [sdk, user, notifier] = yield* Effect.all([
698
+ SDKCore,
699
+ CurrentRestAPIUser,
700
+ Notifications,
701
+ ]);
702
+
703
+ const { rank } = user;
704
+
705
+ if (rank !== 'owner' && rank !== 'admin') {
706
+ return yield* new RestAPIError({ error: 'Unauthorized' });
707
+ }
708
+
709
+ const existingPage = yield* sdk.GET.page.byId(id);
710
+
711
+ if (!existingPage) {
712
+ return yield* new RestAPIError({ error: 'Page not found' });
713
+ }
714
+
715
+ if (!slug || existingPage.slug !== slug) {
716
+ return yield* new RestAPIError({
717
+ error: 'Slug is required and must match the existing page slug for deletion',
718
+ });
719
+ }
720
+
721
+ yield* sdk.DELETE.page(id);
722
+ yield* sdk.CLEAR.page.byId(id);
723
+
724
+ yield* notifier
725
+ .sendEditorNotification('page_deleted', existingPage.title)
726
+ .pipe(
727
+ Effect.catchAll(
728
+ () => new RestAPIError({ error: 'Failed to send notification for page deletion' })
729
+ )
730
+ );
731
+
732
+ return {
733
+ message: 'Page deleted successfully',
734
+ };
735
+ },
736
+ Notifications.Provide,
737
+ Effect.catchTags({
738
+ ...sharedDBErrors,
739
+ ...sharedNotifierErrors,
740
+ ...sharedPageCollectionErrors,
741
+ })
742
+ )
743
+ )
744
+ .handle(
745
+ 'updatePage',
746
+ Effect.fn(
747
+ function* ({ path: { id }, payload }) {
748
+ if (!restAPIEnabled) {
749
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
750
+ }
751
+ const [sdk, user, notifier] = yield* Effect.all([
752
+ SDKCore,
753
+ CurrentRestAPIUser,
754
+ Notifications,
755
+ ]);
756
+
757
+ const { rank } = user;
758
+
759
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
760
+ return yield* new RestAPIError({ error: 'Unauthorized' });
761
+ }
762
+
763
+ const { data, content } = payload;
764
+
765
+ if (!data) {
766
+ return yield* new RestAPIError({ error: 'Page data is required' });
767
+ }
768
+
769
+ if (!content) {
770
+ return yield* new RestAPIError({ error: 'Page content is required' });
771
+ }
772
+
773
+ if (!data.id) {
774
+ return yield* new RestAPIError({ error: 'Page ID is required in page data' });
775
+ }
776
+
777
+ if (data.id && data.id !== id) {
778
+ return yield* new RestAPIError({
779
+ error: "ID in payload does not match ID in path, ID's must match to update.",
780
+ });
781
+ }
782
+
783
+ const existingPage = yield* sdk.GET.page.byId(id);
784
+
785
+ if (!existingPage) {
786
+ return yield* new RestAPIError({ error: 'Page not found' });
787
+ }
788
+
789
+ const { authorId, contributorIds, defaultContent } = existingPage;
790
+
791
+ let AuthorId = authorId;
792
+
793
+ if (!authorId) {
794
+ AuthorId = user.userId;
795
+ }
796
+
797
+ const ContributorIds = contributorIds || [];
798
+
799
+ if (!ContributorIds.includes(user.userId)) {
800
+ ContributorIds.push(user.userId);
801
+ }
802
+
803
+ const newData: tsPageData['Insert']['Type'] = {
804
+ ...(data as tsPageDataSelect),
805
+ authorId: AuthorId,
806
+ contributorIds: yield* encodeStringArray(ContributorIds),
807
+ updatedAt: new Date().toISOString(),
808
+ publishedAt:
809
+ existingPage.draft && data.draft === false
810
+ ? new Date().toISOString()
811
+ : existingPage.publishedAt?.toISOString() || new Date().toISOString(),
812
+ categories: yield* encodeStringArray(data.categories || []),
813
+ tags: yield* encodeStringArray(data.tags || []),
814
+ augments: yield* encodeStringArray(data.augments || []),
815
+ };
816
+
817
+ const getMetaData = sdk.dbService.withCodec({
818
+ encoder: Schema.String,
819
+ decoder: StudioCMSPageData.Select,
820
+ callbackFn: (query, input) =>
821
+ query((db) =>
822
+ db
823
+ .selectFrom('StudioCMSPageData')
824
+ .selectAll()
825
+ .where('id', '=', input)
826
+ .executeTakeFirstOrThrow()
827
+ ),
828
+ });
829
+
830
+ const startMetaData = yield* getMetaData(data.id);
831
+
832
+ yield* sdk.UPDATE.page.byId(data.id, {
833
+ pageData: newData,
834
+ pageContent: content as tsPageContentSelect,
835
+ });
836
+
837
+ const updatedMetaData = yield* getMetaData(data.id);
838
+
839
+ const siteConfig = yield* sdk.GET.siteConfig();
840
+
841
+ if (!siteConfig) {
842
+ return yield* new RestAPIError({ error: 'Site configuration not found' });
843
+ }
844
+
845
+ const { enableDiffs, diffPerPage = 10 } = siteConfig.data;
846
+
847
+ if (enableDiffs) {
848
+ yield* sdk.diffTracking.insert(
849
+ user.userId,
850
+ data.id,
851
+ {
852
+ content: {
853
+ start: defaultContent?.content || '',
854
+ end: content.content || '',
855
+ },
856
+ metaData: {
857
+ start: startMetaData,
858
+ end: updatedMetaData,
859
+ },
860
+ },
861
+ diffPerPage
862
+ );
863
+ }
864
+
865
+ yield* sdk.CLEAR.page.byId(id);
866
+
867
+ yield* notifier
868
+ .sendEditorNotification('page_updated', newData.title)
869
+ .pipe(
870
+ Effect.catchAll(
871
+ () => new RestAPIError({ error: 'Failed to send notification for page update' })
872
+ )
873
+ );
874
+
875
+ return {
876
+ message: 'Page updated successfully',
877
+ };
878
+ },
879
+ Notifications.Provide,
880
+ Effect.catchTags({
881
+ ...sharedDBErrors,
882
+ ...sharedNotifierErrors,
883
+ ...sharedPageCollectionErrors,
884
+ ParsersError: () =>
885
+ new RestAPIError({ error: 'Failed to parse page data during update' }),
886
+ DiffError: () => new RestAPIError({ error: 'Failed to track changes for page update' }),
887
+ })
888
+ )
889
+ )
890
+ .handle(
891
+ 'getPages',
892
+ Effect.fn(
893
+ function* ({ urlParams: { author, draft, parentFolder, published, slug, title } }) {
894
+ if (!restAPIEnabled) {
895
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
896
+ }
897
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
898
+
899
+ if (user.rank !== 'owner' && user.rank !== 'admin' && user.rank !== 'editor') {
900
+ return yield* new RestAPIError({ error: 'Unauthorized' });
901
+ }
902
+
903
+ const allPages = yield* sdk.GET.pages(true);
904
+
905
+ let filteredPages = allPages;
906
+
907
+ if (author) {
908
+ filteredPages = filteredPages.filter((page) => page.authorId === author);
909
+ }
910
+ if (typeof draft === 'boolean') {
911
+ filteredPages = filteredPages.filter((page) => page.draft === draft);
912
+ }
913
+ if (parentFolder) {
914
+ filteredPages = filteredPages.filter((page) => page.parentFolder === parentFolder);
915
+ }
916
+ if (typeof published === 'boolean') {
917
+ filteredPages = filteredPages.filter((page) => !page.draft);
918
+ }
919
+ if (slug) {
920
+ filteredPages = filteredPages.filter((page) => page.slug === slug);
921
+ }
922
+ if (title) {
923
+ filteredPages = filteredPages.filter((page) => page.title.includes(title));
924
+ }
925
+
926
+ return filteredPages;
927
+ },
928
+ Effect.catchTags({
929
+ ...sharedDBErrors,
930
+ ...sharedPageCollectionErrors,
931
+ })
932
+ )
933
+ )
934
+ .handle(
935
+ 'getPage',
936
+ Effect.fn(
937
+ function* ({ path: { id } }) {
938
+ if (!restAPIEnabled) {
939
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
940
+ }
941
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
942
+
943
+ if (user.rank !== 'owner' && user.rank !== 'admin' && user.rank !== 'editor') {
944
+ return yield* new RestAPIError({ error: 'Unauthorized' });
945
+ }
946
+
947
+ const page = yield* sdk.GET.page.byId(id);
948
+
949
+ if (!page) {
950
+ return yield* new RestAPIError({ error: 'Page not found' });
951
+ }
952
+
953
+ return page;
954
+ },
955
+ Effect.catchTags({
956
+ ...sharedDBErrors,
957
+ ...sharedPageCollectionErrors,
958
+ })
959
+ )
960
+ )
961
+ .handle(
962
+ 'getPageHistory',
963
+ Effect.fn(
964
+ function* ({ path: { id }, urlParams: { limit: _limit } }) {
965
+ if (!restAPIEnabled) {
966
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
967
+ }
968
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
969
+
970
+ if (user.rank !== 'owner' && user.rank !== 'admin' && user.rank !== 'editor') {
971
+ return yield* new RestAPIError({ error: 'Unauthorized' });
972
+ }
973
+
974
+ const page = yield* sdk.GET.page.byId(id);
975
+
976
+ if (!page) {
977
+ return yield* new RestAPIError({ error: 'Page not found' });
978
+ }
979
+
980
+ const limit = _limit && _limit > 0 ? Math.min(_limit, 100) : undefined;
981
+
982
+ const diffs = limit
983
+ ? yield* sdk.diffTracking.get.byPageId.latest(id, limit)
984
+ : yield* sdk.diffTracking.get.byPageId.all(id);
985
+
986
+ return diffs;
987
+ },
988
+ Effect.catchTags({
989
+ ...sharedDBErrors,
990
+ ...sharedPageCollectionErrors,
991
+ ParsersError: () => new RestAPIError({ error: 'Failed to parse page history data' }),
992
+ })
993
+ )
994
+ )
995
+ .handle(
996
+ 'getPageHistoryEntry',
997
+ Effect.fn(
998
+ function* ({ path: { id, diffId } }) {
999
+ if (!restAPIEnabled) {
1000
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1001
+ }
1002
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
1003
+
1004
+ if (user.rank !== 'owner' && user.rank !== 'admin' && user.rank !== 'editor') {
1005
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1006
+ }
1007
+
1008
+ const diff = yield* sdk.diffTracking.get.single(diffId);
1009
+
1010
+ if (!diff) {
1011
+ return yield* new RestAPIError({ error: 'Diff entry not found' });
1012
+ }
1013
+
1014
+ if (diff.pageId !== id) {
1015
+ return yield* new RestAPIError({
1016
+ error: 'Diff entry does not belong to the specified page',
1017
+ });
1018
+ }
1019
+
1020
+ return diff;
1021
+ },
1022
+ Effect.catchTags({
1023
+ ...sharedDBErrors,
1024
+ ParsersError: () => new RestAPIError({ error: 'Failed to parse diff data' }),
1025
+ })
1026
+ )
1027
+ )
1028
+
1029
+ // Settings Endpoints
1030
+ .handle(
1031
+ 'getSettings',
1032
+ Effect.fn(
1033
+ function* () {
1034
+ if (!restAPIEnabled) {
1035
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1036
+ }
1037
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
1038
+
1039
+ if (user.rank !== 'owner') {
1040
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1041
+ }
1042
+
1043
+ const siteConfig = yield* sdk.GET.siteConfig();
1044
+
1045
+ if (!siteConfig) {
1046
+ return yield* new RestAPIError({ error: 'Site configuration not found' });
1047
+ }
1048
+
1049
+ return siteConfig;
1050
+ },
1051
+ Effect.catchTags({
1052
+ ...sharedDBErrors,
1053
+ UnknownException: () =>
1054
+ new RestAPIError({
1055
+ error: 'An unknown error occurred while retrieving site configuration',
1056
+ }),
1057
+ })
1058
+ )
1059
+ )
1060
+ .handle(
1061
+ 'updateSettings',
1062
+ Effect.fn(function* ({ payload }) {
1063
+ if (!restAPIEnabled) {
1064
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1065
+ }
1066
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
1067
+
1068
+ if (user.rank !== 'owner') {
1069
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1070
+ }
1071
+
1072
+ if (!payload.title || payload.title.trim() === '') {
1073
+ return yield* new RestAPIError({ error: 'Invalid form data, title is required' });
1074
+ }
1075
+
1076
+ if (!payload.description || payload.description.trim() === '') {
1077
+ return yield* new RestAPIError({
1078
+ error: 'Invalid form data, description is required',
1079
+ });
1080
+ }
1081
+
1082
+ if (!payload.loginPageBackground || payload.loginPageBackground.trim() === '') {
1083
+ return yield* new RestAPIError({
1084
+ error: 'Invalid form data, loginPageBackground is required',
1085
+ });
1086
+ }
1087
+
1088
+ if (
1089
+ payload.loginPageBackground === 'custom' &&
1090
+ (!payload.loginPageCustomImage || payload.loginPageCustomImage.trim() === '')
1091
+ ) {
1092
+ return yield* new RestAPIError({
1093
+ error:
1094
+ 'Invalid form data, loginPageCustomImage is required when loginPageBackground is set to custom',
1095
+ });
1096
+ }
1097
+
1098
+ yield* sdk.UPDATE.siteConfig(payload);
1099
+
1100
+ return {
1101
+ message: 'Site configuration updated successfully',
1102
+ };
1103
+ }, Effect.catchTags(sharedDBErrors))
1104
+ )
1105
+
1106
+ // Tag Endpoints
1107
+ .handle(
1108
+ 'createTag',
1109
+ Effect.fn(
1110
+ function* ({ payload }) {
1111
+ if (!restAPIEnabled) {
1112
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1113
+ }
1114
+ const [sdk, user, notifier] = yield* Effect.all([
1115
+ SDKCore,
1116
+ CurrentRestAPIUser,
1117
+ Notifications,
1118
+ ]);
1119
+
1120
+ const { rank } = user;
1121
+
1122
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
1123
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1124
+ }
1125
+
1126
+ const newId = yield* sdk.UTIL.Generators.generateRandomIDNumber(9).pipe(
1127
+ Effect.catchAll(
1128
+ () =>
1129
+ new RestAPIError({
1130
+ error: 'Failed to generate unique ID for new tag',
1131
+ })
1132
+ )
1133
+ );
1134
+
1135
+ const newTag = {
1136
+ id: newId,
1137
+ ...payload,
1138
+ };
1139
+
1140
+ yield* sdk.POST.databaseEntry.tags(newTag);
1141
+
1142
+ yield* notifier
1143
+ .sendEditorNotification('new_tag', newTag.name)
1144
+ .pipe(
1145
+ Effect.catchAll(
1146
+ () => new RestAPIError({ error: 'Failed to send notification for new tag' })
1147
+ )
1148
+ );
1149
+
1150
+ const inserted = yield* sdk.GET.tags.byId(newId);
1151
+
1152
+ if (!inserted) {
1153
+ return yield* new RestAPIError({ error: 'Failed to retrieve newly created tag' });
1154
+ }
1155
+
1156
+ return inserted;
1157
+ },
1158
+ Notifications.Provide,
1159
+ Effect.catchTags({
1160
+ ...sharedDBErrors,
1161
+ ...sharedNotifierErrors,
1162
+ GeneratorError: () =>
1163
+ new RestAPIError({ error: 'Failed to generate unique ID for new tag' }),
1164
+ })
1165
+ )
1166
+ )
1167
+ .handle(
1168
+ 'deleteTag',
1169
+ Effect.fn(
1170
+ function* ({ path: { id } }) {
1171
+ if (!restAPIEnabled) {
1172
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1173
+ }
1174
+ const [sdk, user, notifier] = yield* Effect.all([
1175
+ SDKCore,
1176
+ CurrentRestAPIUser,
1177
+ Notifications,
1178
+ ]);
1179
+
1180
+ const { rank } = user;
1181
+
1182
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
1183
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1184
+ }
1185
+
1186
+ const getPageList = sdk.GET.pages(true, true);
1187
+
1188
+ const flattenAndCount = <T extends { id: number }>(arrays: T[][]): boolean => {
1189
+ return arrays.flat().filter((data) => data.id === id).length > 0;
1190
+ };
1191
+
1192
+ const checkForChildrenPagesTags = () =>
1193
+ getPageList.pipe(
1194
+ Effect.map((data) => data.map(({ tags }) => tags)),
1195
+ Effect.map(flattenAndCount)
1196
+ );
1197
+
1198
+ const existingTag = yield* sdk.GET.tags.byId(id);
1199
+
1200
+ if (!existingTag) {
1201
+ return yield* new RestAPIError({ error: 'Tag not found' });
1202
+ }
1203
+
1204
+ const hasChildrenPages = yield* checkForChildrenPagesTags();
1205
+
1206
+ if (hasChildrenPages) {
1207
+ return yield* new RestAPIError({ error: 'Cannot delete tag assigned to pages' });
1208
+ }
1209
+
1210
+ yield* sdk.DELETE.tags(id);
1211
+
1212
+ yield* notifier
1213
+ .sendEditorNotification('delete_tag', existingTag.name)
1214
+ .pipe(
1215
+ Effect.catchAll(
1216
+ () => new RestAPIError({ error: 'Failed to send notification for tag deletion' })
1217
+ )
1218
+ );
1219
+
1220
+ return { success: true };
1221
+ },
1222
+ Notifications.Provide,
1223
+ Effect.catchTags({
1224
+ ...sharedDBErrors,
1225
+ ...sharedNotifierErrors,
1226
+ ...sharedPageCollectionErrors,
1227
+ })
1228
+ )
1229
+ )
1230
+ .handle(
1231
+ 'updateTag',
1232
+ Effect.fn(
1233
+ function* ({ path: { id }, payload }) {
1234
+ if (!restAPIEnabled) {
1235
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1236
+ }
1237
+ const [sdk, user, notifier] = yield* Effect.all([
1238
+ SDKCore,
1239
+ CurrentRestAPIUser,
1240
+ Notifications,
1241
+ ]);
1242
+
1243
+ const { rank } = user;
1244
+
1245
+ if (rank !== 'owner' && rank !== 'admin' && rank !== 'editor') {
1246
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1247
+ }
1248
+
1249
+ const updateTag = sdk.dbService.withCodec({
1250
+ encoder: Schema.partial(StudioCMSPageDataTags.Select),
1251
+ decoder: StudioCMSPageDataTags.Select,
1252
+ callbackFn: (db, data) =>
1253
+ db((client) =>
1254
+ client.transaction().execute(async (trx) => {
1255
+ await trx
1256
+ .updateTable('StudioCMSPageDataTags')
1257
+ .set(data)
1258
+ .where('id', '=', id)
1259
+ .executeTakeFirstOrThrow();
1260
+
1261
+ return await trx
1262
+ .selectFrom('StudioCMSPageDataTags')
1263
+ .selectAll()
1264
+ .where('id', '=', id)
1265
+ .executeTakeFirstOrThrow();
1266
+ })
1267
+ ),
1268
+ });
1269
+
1270
+ if (payload.id && payload.id !== id) {
1271
+ return yield* new RestAPIError({
1272
+ error: "ID in payload does not match ID in path, ID's must match to update.",
1273
+ });
1274
+ }
1275
+
1276
+ const data = yield* updateTag(payload);
1277
+ yield* notifier
1278
+ .sendEditorNotification('update_tag', data.name)
1279
+ .pipe(
1280
+ Effect.catchAll(
1281
+ () => new RestAPIError({ error: 'Failed to send notification for tag update' })
1282
+ )
1283
+ );
1284
+
1285
+ return data;
1286
+ },
1287
+ Notifications.Provide,
1288
+ Effect.catchTags({
1289
+ ...sharedDBErrors,
1290
+ ...sharedNotifierErrors,
1291
+ })
1292
+ )
1293
+ )
1294
+ .handle(
1295
+ 'getTags',
1296
+ Effect.fn(function* ({ urlParams: { name } }) {
1297
+ if (!restAPIEnabled) {
1298
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1299
+ }
1300
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
1301
+
1302
+ if (user.rank !== 'owner' && user.rank !== 'admin' && user.rank !== 'editor') {
1303
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1304
+ }
1305
+
1306
+ let tags = yield* sdk.GET.tags.getAll();
1307
+
1308
+ if (name) {
1309
+ tags = tags.filter((tag) => tag.name.includes(name));
1310
+ }
1311
+
1312
+ return tags;
1313
+ }, Effect.catchTags(sharedDBErrors))
1314
+ )
1315
+ .handle(
1316
+ 'getTag',
1317
+ Effect.fn(function* ({ path: { id } }) {
1318
+ if (!restAPIEnabled) {
1319
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1320
+ }
1321
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
1322
+
1323
+ if (user.rank !== 'owner' && user.rank !== 'admin' && user.rank !== 'editor') {
1324
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1325
+ }
1326
+
1327
+ const tag = yield* sdk.GET.tags.byId(id);
1328
+
1329
+ if (!tag) {
1330
+ return yield* new RestAPIError({ error: 'Tag not found' });
1331
+ }
1332
+
1333
+ return tag;
1334
+ }, Effect.catchTags(sharedDBErrors))
1335
+ )
1336
+
1337
+ // User Endpoints
1338
+ .handle(
1339
+ 'createUser',
1340
+ Effect.fn(
1341
+ function* ({ payload: { username, password, email, displayname, rank: newUserRank } }) {
1342
+ if (!restAPIEnabled) {
1343
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1344
+ }
1345
+ const [sdk, user, userUtils, passwordUtils, notifier] = yield* Effect.all([
1346
+ SDKCore,
1347
+ CurrentRestAPIUser,
1348
+ User,
1349
+ Password,
1350
+ Notifications,
1351
+ ]);
1352
+
1353
+ const { rank } = user;
1354
+
1355
+ if (rank !== 'owner' && rank !== 'admin') {
1356
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1357
+ }
1358
+
1359
+ if (!username || !email || !displayname || !newUserRank) {
1360
+ return yield* new RestAPIError({
1361
+ error: 'Missing required fields: username, email, displayname',
1362
+ });
1363
+ }
1364
+
1365
+ if (newUserRank === 'owner' && rank !== 'owner') {
1366
+ return yield* new RestAPIError({
1367
+ error: 'Unauthorized to create user with owner rank',
1368
+ });
1369
+ }
1370
+
1371
+ if (!password) {
1372
+ password = yield* sdk.UTIL.Generators.generateRandomPassword(12);
1373
+ }
1374
+
1375
+ if (rank === 'admin' && newUserRank === 'owner') {
1376
+ return yield* new RestAPIError({
1377
+ error: 'Unauthorized to create user with owner rank',
1378
+ });
1379
+ }
1380
+
1381
+ const checkEmail = isValidEmail(email);
1382
+
1383
+ if (!checkEmail.success) {
1384
+ return yield* new RestAPIError({
1385
+ error: `Invalid email format: ${checkEmail.error.message}`,
1386
+ });
1387
+ }
1388
+
1389
+ const [
1390
+ verifyUsernameResponse,
1391
+ verifyPasswordResponse,
1392
+ { usernameSearch, emailSearch },
1393
+ ] = yield* Effect.all([
1394
+ userUtils.verifyUsernameInput(username),
1395
+ passwordUtils.verifyPasswordStrength(password),
1396
+ sdk.AUTH.user.searchUsersForUsernameOrEmail(username, checkEmail.data),
1397
+ ]);
1398
+
1399
+ if (verifyUsernameResponse !== true) {
1400
+ return yield* new RestAPIError({ error: verifyUsernameResponse });
1401
+ }
1402
+
1403
+ if (verifyPasswordResponse !== true) {
1404
+ return yield* new RestAPIError({ error: verifyPasswordResponse });
1405
+ }
1406
+
1407
+ if (usernameSearch.length > 0) {
1408
+ return yield* new RestAPIError({ error: 'Username already exists' });
1409
+ }
1410
+ if (emailSearch.length > 0) {
1411
+ return yield* new RestAPIError({ error: 'Email already exists' });
1412
+ }
1413
+
1414
+ // Create a new user
1415
+ const newUser = yield* userUtils.createLocalUser(
1416
+ displayname,
1417
+ username,
1418
+ checkEmail.data,
1419
+ password
1420
+ );
1421
+ yield* sdk.UPDATE.permissions({
1422
+ user: newUser.id,
1423
+ rank: newUserRank,
1424
+ });
1425
+ yield* notifier
1426
+ .sendAdminNotification('new_user', newUser.username)
1427
+ .pipe(
1428
+ Effect.catchAll(
1429
+ () => new RestAPIError({ error: 'Failed to send notification for new user' })
1430
+ )
1431
+ );
1432
+
1433
+ return APISafeUserFields.make({
1434
+ username,
1435
+ email: checkEmail.data,
1436
+ name: displayname,
1437
+ createdAt: newUser.createdAt,
1438
+ updatedAt: newUser.updatedAt,
1439
+ avatar: newUser.avatar,
1440
+ url: newUser.url,
1441
+ id: newUser.id,
1442
+ });
1443
+ },
1444
+ Notifications.Provide,
1445
+ Effect.catchTags({
1446
+ ...sharedDBErrors,
1447
+ ...sharedNotifierErrors,
1448
+ })
1449
+ )
1450
+ )
1451
+ .handle(
1452
+ 'deleteUser',
1453
+ Effect.fn(
1454
+ function* ({ path: { id } }) {
1455
+ if (!restAPIEnabled) {
1456
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1457
+ }
1458
+ const [sdk, user, notifier] = yield* Effect.all([
1459
+ SDKCore,
1460
+ CurrentRestAPIUser,
1461
+ Notifications,
1462
+ ]);
1463
+
1464
+ const { rank } = user;
1465
+
1466
+ if (rank !== 'owner' && rank !== 'admin') {
1467
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1468
+ }
1469
+
1470
+ if (user.userId === id) {
1471
+ return yield* new RestAPIError({ error: 'Users cannot delete themselves' });
1472
+ }
1473
+
1474
+ const existingUser = yield* sdk.GET.users.byId(id);
1475
+
1476
+ if (!existingUser) {
1477
+ return yield* new RestAPIError({ error: 'User not found' });
1478
+ }
1479
+
1480
+ const { permissionsData } = existingUser;
1481
+
1482
+ const existingUserRank = permissionsData?.rank ?? 'unknown';
1483
+ const existingUserRankIndex = availablePermissionRanks.indexOf(existingUserRank);
1484
+ const loggedInUserRankIndex = availablePermissionRanks.indexOf(user.rank);
1485
+
1486
+ if (loggedInUserRankIndex <= existingUserRankIndex) {
1487
+ return yield* new RestAPIError({
1488
+ error: 'Unauthorized to delete user with equal or higher rank',
1489
+ });
1490
+ }
1491
+
1492
+ const response = yield* sdk.DELETE.user(id);
1493
+
1494
+ if (!response) {
1495
+ return yield* new RestAPIError({ error: 'Failed to delete user' });
1496
+ }
1497
+
1498
+ if (response.status === 'error') {
1499
+ return yield* new RestAPIError({
1500
+ error: response.message || 'Failed to delete user',
1501
+ });
1502
+ }
1503
+
1504
+ yield* notifier
1505
+ .sendAdminNotification('user_deleted', existingUser.username)
1506
+ .pipe(
1507
+ Effect.catchAll(
1508
+ () => new RestAPIError({ error: 'Failed to send notification for user deletion' })
1509
+ )
1510
+ );
1511
+
1512
+ return {
1513
+ message: response.message || 'User deleted successfully',
1514
+ };
1515
+ },
1516
+ Notifications.Provide,
1517
+ Effect.catchTags({
1518
+ ...sharedDBErrors,
1519
+ ...sharedNotifierErrors,
1520
+ })
1521
+ )
1522
+ )
1523
+ .handle(
1524
+ 'updateUser',
1525
+ Effect.fn(
1526
+ function* ({ path: { id }, payload }) {
1527
+ if (!restAPIEnabled) {
1528
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1529
+ }
1530
+ const [sdk, user, notifier] = yield* Effect.all([
1531
+ SDKCore,
1532
+ CurrentRestAPIUser,
1533
+ Notifications,
1534
+ ]);
1535
+
1536
+ const { rank } = user;
1537
+
1538
+ if (rank !== 'owner' && rank !== 'admin') {
1539
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1540
+ }
1541
+
1542
+ const existingUser = yield* sdk.GET.users.byId(id);
1543
+
1544
+ if (!existingUser) {
1545
+ return yield* new RestAPIError({ error: 'User not found' });
1546
+ }
1547
+
1548
+ const { permissionsData } = existingUser;
1549
+
1550
+ const existingUserRank = permissionsData?.rank ?? 'unknown';
1551
+ const existingUserRankIndex = availablePermissionRanks.indexOf(existingUserRank);
1552
+ const loggedInUserRankIndex = availablePermissionRanks.indexOf(user.rank);
1553
+
1554
+ if (loggedInUserRankIndex <= existingUserRankIndex) {
1555
+ return yield* new RestAPIError({
1556
+ error: 'Unauthorized to update user with higher rank',
1557
+ });
1558
+ }
1559
+
1560
+ if (payload.rank) {
1561
+ const payloadRankIndex = availablePermissionRanks.indexOf(payload.rank);
1562
+
1563
+ if (loggedInUserRankIndex <= payloadRankIndex) {
1564
+ return yield* new RestAPIError({
1565
+ error: 'Unauthorized to set user rank higher than your own',
1566
+ });
1567
+ }
1568
+ }
1569
+
1570
+ const updatedRank = yield* sdk.UPDATE.permissions({
1571
+ user: id,
1572
+ rank: payload.rank,
1573
+ });
1574
+
1575
+ if (!updatedRank) {
1576
+ return yield* new RestAPIError({ error: 'Failed to update user rank' });
1577
+ }
1578
+
1579
+ const updatedUser = yield* sdk.GET.users.byId(id);
1580
+
1581
+ if (!updatedUser) {
1582
+ return yield* new RestAPIError({ error: 'Failed to retrieve updated user data' });
1583
+ }
1584
+
1585
+ yield* Effect.all([
1586
+ notifier.sendUserNotification('account_updated', updatedUser.id),
1587
+ notifier.sendAdminNotification('user_updated', updatedUser.username),
1588
+ ]).pipe(
1589
+ Effect.catchAll(
1590
+ () => new RestAPIError({ error: 'Failed to send notification for user update' })
1591
+ )
1592
+ );
1593
+
1594
+ return APISafeUserFields.make(updatedUser);
1595
+ },
1596
+ Notifications.Provide,
1597
+ Effect.catchTags({
1598
+ ...sharedDBErrors,
1599
+ ...sharedNotifierErrors,
1600
+ })
1601
+ )
1602
+ )
1603
+ .handle(
1604
+ 'getUsers',
1605
+ Effect.fn(
1606
+ function* ({ urlParams: { name, rank, username } }) {
1607
+ if (!restAPIEnabled) {
1608
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1609
+ }
1610
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
1611
+
1612
+ if (user.rank !== 'owner' && user.rank !== 'admin') {
1613
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1614
+ }
1615
+
1616
+ const allUsers = yield* sdk.GET.users.all();
1617
+
1618
+ let data = allUsers.map(
1619
+ ({
1620
+ avatar,
1621
+ createdAt,
1622
+ email,
1623
+ id,
1624
+ name,
1625
+ permissionsData,
1626
+ updatedAt,
1627
+ url,
1628
+ username,
1629
+ }) => ({
1630
+ avatar,
1631
+ createdAt,
1632
+ email,
1633
+ id,
1634
+ name,
1635
+ rank: permissionsData?.rank ?? 'unknown',
1636
+ updatedAt,
1637
+ url,
1638
+ username,
1639
+ })
1640
+ );
1641
+
1642
+ if (rank !== 'owner') {
1643
+ data = data.filter((user) => user.rank !== 'owner');
1644
+ }
1645
+
1646
+ if (name) {
1647
+ data = data.filter((user) => user.name.toLowerCase().includes(name.toLowerCase()));
1648
+ }
1649
+ if (rank) {
1650
+ data = data.filter((user) => user.rank === rank);
1651
+ }
1652
+ if (username) {
1653
+ data = data.filter((user) =>
1654
+ user.username.toLowerCase().includes(username.toLowerCase())
1655
+ );
1656
+ }
1657
+
1658
+ return data;
1659
+ },
1660
+ Effect.catchTags({
1661
+ ...sharedDBErrors,
1662
+ NotFoundError: () => new RestAPIError({ error: 'No users found' }),
1663
+ QueryError: () => new RestAPIError({ error: 'Failed to query user data' }),
1664
+ QueryParseError: () => new RestAPIError({ error: 'Failed to parse user data' }),
1665
+ })
1666
+ )
1667
+ )
1668
+ .handle(
1669
+ 'getUser',
1670
+ Effect.fn(
1671
+ function* ({ path: { id } }) {
1672
+ if (!restAPIEnabled) {
1673
+ return yield* new RestAPIError({ error: 'Endpoint not found' });
1674
+ }
1675
+ const [sdk, user] = yield* Effect.all([SDKCore, CurrentRestAPIUser]);
1676
+
1677
+ const { rank } = user;
1678
+
1679
+ if (rank !== 'owner' && rank !== 'admin') {
1680
+ return yield* new RestAPIError({ error: 'Unauthorized' });
1681
+ }
1682
+
1683
+ const existingUser = yield* sdk.GET.users.byId(id);
1684
+
1685
+ if (!existingUser) {
1686
+ return yield* new RestAPIError({ error: 'User not found' });
1687
+ }
1688
+
1689
+ const { avatar, createdAt, email, name, permissionsData, updatedAt, url, username } =
1690
+ existingUser;
1691
+
1692
+ const existingUserRank = (permissionsData?.rank ??
1693
+ 'visitor') as AvailablePermissionRanks;
1694
+
1695
+ const data = {
1696
+ avatar,
1697
+ createdAt,
1698
+ email,
1699
+ id,
1700
+ name,
1701
+ rank: existingUserRank,
1702
+ updatedAt,
1703
+ url,
1704
+ username,
1705
+ };
1706
+
1707
+ const existingUserRankIndex = availablePermissionRanks.indexOf(existingUserRank);
1708
+ const loggedInUserRankIndex = availablePermissionRanks.indexOf(user.rank);
1709
+
1710
+ if (loggedInUserRankIndex <= existingUserRankIndex) {
1711
+ return yield* new RestAPIError({
1712
+ error: 'Unauthorized to view user with higher rank',
1713
+ });
1714
+ }
1715
+
1716
+ return data;
1717
+ },
1718
+ Effect.catchTags({
1719
+ ...sharedDBErrors,
1720
+ QueryError: () => new RestAPIError({ error: 'Failed to query user data' }),
1721
+ QueryParseError: () => new RestAPIError({ error: 'Failed to parse user data' }),
1722
+ NotFoundError: () => new RestAPIError({ error: 'User not found' }),
1723
+ })
1724
+ )
1725
+ )
1726
+ ).pipe(Layer.provide(RestAPIAuthorizationLive));