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.
- package/CHANGELOG.md +122 -0
- package/dist/cli/add/index.d.ts +2 -2
- package/dist/cli/add/index.js +4 -3
- package/dist/cli/add/npm-utils.d.ts +6 -6
- package/dist/cli/add/tryToInstallPlugins.d.ts +1 -1
- package/dist/cli/add/tryToInstallPlugins.js +6 -5
- package/dist/cli/add/updateStudioCMSConfig.d.ts +1 -1
- package/dist/cli/add/updateStudioCMSConfig.js +3 -4
- package/dist/cli/add/validatePlugins.d.ts +1 -2
- package/dist/cli/add/validatePlugins.js +15 -9
- package/dist/cli/crypto/genJWT/index.d.ts +1 -1
- package/dist/cli/crypto/genJWT/index.js +8 -9
- package/dist/cli/crypto/index.d.ts +1 -1
- package/dist/cli/init/steps/env.js +14 -4
- package/dist/cli/init/steps/next.d.ts +1 -1
- package/dist/cli/init/steps/next.js +6 -5
- package/dist/cli/migrator/index.d.ts +1 -1
- package/dist/cli/migrator/index.js +2 -2
- package/dist/cli/users/index.d.ts +1 -1
- package/dist/cli/users/shared.js +2 -2
- package/dist/cli/users/steps/createUsers.js +7 -7
- package/dist/cli/users/steps/modifyUsers.js +2 -2
- package/dist/cli/users/steps/next.d.ts +1 -1
- package/dist/cli/utils/checkRequiredEnvVars.js +2 -2
- package/dist/cli/utils/context.d.ts +2 -4
- package/dist/cli/utils/context.js +1 -3
- package/dist/cli/utils/getCliDbClient.d.ts +1 -1
- package/dist/cli/utils/intro.d.ts +1 -1
- package/dist/cli/utils/loadConfig.d.ts +54 -49
- package/dist/cli/utils/loadConfig.js +5 -8
- package/dist/cli/utils/logger.js +3 -3
- package/dist/cli/utils/user-utils.d.ts +1 -1
- package/dist/cli/utils/user-utils.js +4 -3
- package/dist/client/apiClient.d.ts +4923 -0
- package/dist/client/apiClient.js +72 -0
- package/dist/config.d.ts +1734 -1
- package/dist/consts.d.ts +5 -5
- package/dist/consts.js +3 -2
- package/dist/db/plugins.d.ts +1 -1
- package/dist/db/plugins.js +5 -8
- package/dist/handlers/frontend/routes.d.ts +4 -18
- package/dist/handlers/frontend/routes.js +13 -152
- package/dist/handlers/frontend/types.d.ts +1 -1
- package/dist/handlers/frontend/utils.js +0 -18
- package/dist/handlers/pluginHandler.d.ts +34 -257
- package/dist/handlers/pluginHandler.js +92 -46
- package/dist/handlers/routeHandler.js +32 -11
- package/dist/handlers/setupDbStudio.d.ts +3 -1
- package/dist/handlers/setupDbStudio.js +19 -10
- package/dist/handlers/storage-manager/core/effectify-astro-context.d.ts +25 -0
- package/dist/handlers/storage-manager/core/effectify-astro-context.js +78 -0
- package/dist/handlers/storage-manager/no-op.d.ts +2 -2
- package/dist/handlers/storage-manager/no-op.js +2 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.js +10 -20
- package/dist/integrations/robots/index.d.ts +2 -2
- package/dist/integrations/robots/index.js +1 -3
- package/dist/integrations/robots/schema.d.ts +102 -273
- package/dist/integrations/robots/schema.js +220 -209
- package/dist/plugins/analytics/assets/schemas.d.ts +14 -9
- package/dist/plugins/analytics/assets/schemas.js +25 -17
- package/dist/plugins/analytics/db-client.d.ts +1 -1
- package/dist/plugins/analytics/index.d.ts +823 -3
- package/dist/plugins/analytics/index.js +4 -5
- package/dist/plugins/analytics/schemas.d.ts +54 -62
- package/dist/plugins/analytics/schemas.js +64 -13
- package/dist/plugins/analytics/table.d.ts +1 -1
- package/dist/plugins.d.ts +0 -1
- package/dist/schemas/config/api.d.ts +17 -0
- package/dist/schemas/config/api.js +14 -0
- package/dist/schemas/config/auth.d.ts +55 -59
- package/dist/schemas/config/auth.js +34 -11
- package/dist/schemas/config/dashboard.d.ts +43 -79
- package/dist/schemas/config/dashboard.js +43 -12
- package/dist/schemas/config/db.d.ts +15 -17
- package/dist/schemas/config/db.js +13 -5
- package/dist/schemas/config/developer.d.ts +33 -45
- package/dist/schemas/config/developer.js +22 -5
- package/dist/schemas/config/index.d.ts +398 -521
- package/dist/schemas/config/index.js +115 -57
- package/dist/schemas/config/sdk.d.ts +50 -196
- package/dist/schemas/config/sdk.js +61 -73
- package/dist/schemas/custom.d.ts +40 -0
- package/dist/schemas/custom.js +41 -0
- package/dist/schemas/external-schemas.d.ts +171 -0
- package/dist/schemas/external-schemas.js +179 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +2 -0
- package/dist/schemas/plugins/i18n.d.ts +59 -39
- package/dist/schemas/plugins/i18n.js +42 -5
- package/dist/schemas/plugins/index.d.ts +7126 -10296
- package/dist/schemas/plugins/index.js +260 -276
- package/dist/schemas/plugins/shared.d.ts +1293 -3718
- package/dist/schemas/plugins/shared.js +320 -329
- package/dist/test-utils.d.ts +15 -4
- package/dist/test-utils.js +27 -11
- package/dist/toolbar/db-viewer/db-shared-types.d.ts +6 -6
- package/dist/toolbar/db-viewer/studio/connection.d.ts +8 -4
- package/dist/toolbar/db-viewer/studio/connection.js +2 -28
- package/dist/toolbar/db-viewer/studio/env/libsql.d.ts +7 -0
- package/dist/toolbar/db-viewer/studio/env/libsql.js +17 -0
- package/dist/toolbar/db-viewer/studio/env/mysql.d.ts +7 -0
- package/dist/toolbar/db-viewer/studio/env/mysql.js +23 -0
- package/dist/toolbar/db-viewer/studio/env/postgres.d.ts +7 -0
- package/dist/toolbar/db-viewer/studio/env/postgres.js +23 -0
- package/dist/toolbar/db-viewer/studio/index.js +20 -56
- package/dist/toolbar/db-viewer/studio/type.d.ts +1 -2
- package/dist/toolbar/db-viewer/studio/virtual-connection/libsql.d.ts +3 -0
- package/dist/toolbar/db-viewer/studio/virtual-connection/libsql.js +24 -0
- package/dist/toolbar/db-viewer/studio/virtual-connection/mysql.d.ts +3 -0
- package/dist/toolbar/db-viewer/studio/virtual-connection/mysql.js +9 -0
- package/dist/toolbar/db-viewer/studio/virtual-connection/postgres.d.ts +3 -0
- package/dist/toolbar/db-viewer/studio/virtual-connection/postgres.js +9 -0
- package/dist/toolbar/db-viewer/viewer.js +20 -21
- package/dist/types.d.ts +30 -0
- package/dist/utils/effects/smtp.d.ts +1 -1
- package/dist/utils/lang-helper.d.ts +10 -2
- package/dist/virtual.d.ts +35 -28
- package/dist/virtuals/auth/core.d.ts +5 -5
- package/dist/virtuals/auth/verify-email.d.ts +6 -6
- package/dist/virtuals/components/Generator.astro +2 -2
- package/dist/virtuals/components/Renderer.astro +9 -1
- package/dist/virtuals/components/renderFn.d.ts +3 -1
- package/dist/virtuals/components/renderFn.js +18 -0
- package/dist/virtuals/lib/headDefaults.d.ts +4 -2
- package/dist/virtuals/lib/headDefaults.js +0 -2
- package/dist/virtuals/lib/routeMap.d.ts +0 -12
- package/dist/virtuals/lib/routeMap.js +2 -14
- package/dist/virtuals/mailer/index.d.ts +3 -3
- package/dist/virtuals/notifier/index.d.ts +5 -5
- package/dist/virtuals/plugins/dashboard-pages.d.ts +2 -64
- package/dist/virtuals/scripts/StorageFileBrowser.d.ts +1 -172
- package/dist/virtuals/scripts/StorageFileBrowser.js +216 -119
- package/dist/virtuals/template-engine/index.d.ts +4 -4
- package/frontend/components/dashboard/configuration/ConfigForm.astro +218 -110
- package/frontend/components/dashboard/content-mgmt/ContentSearch.astro +21 -22
- package/frontend/components/dashboard/content-mgmt/CreateFolder.astro +66 -54
- package/frontend/components/dashboard/content-mgmt/CreatePage.astro +58 -104
- package/frontend/components/dashboard/content-mgmt/EditFolder.astro +65 -67
- package/frontend/components/dashboard/content-mgmt/EditPage.astro +86 -134
- package/frontend/components/dashboard/content-mgmt/InnerSidebarElement.astro +0 -1
- package/frontend/components/dashboard/content-mgmt/PageHeader.astro +33 -52
- package/frontend/components/dashboard/content-mgmt/PageTypeHandler.astro +2 -2
- package/frontend/components/dashboard/profile/APITokens.astro +219 -158
- package/frontend/components/dashboard/profile/BasicInfo.astro +165 -106
- package/frontend/components/dashboard/profile/Notifications.astro +27 -18
- package/frontend/components/dashboard/profile/UpdatePassword.astro +134 -94
- package/frontend/components/dashboard/sidebar/VersionCheck.astro +31 -16
- package/frontend/components/dashboard/sidebar/VersionCheckChangelog.astro +18 -11
- package/frontend/components/dashboard/sidebar-modals/VersionModal.astro +2 -2
- package/frontend/components/dashboard/smtp-config/TemplateEditor.astro +14 -14
- package/frontend/components/dashboard/taxonomy/InnerSidebarElement.astro +0 -1
- package/frontend/components/dashboard/taxonomy/MetaContainer.astro +0 -2
- package/frontend/components/dashboard/taxonomy/PageHeader.astro +16 -24
- package/frontend/components/dashboard/taxonomy/TaxonomySearch.astro +23 -27
- package/frontend/components/dashboard/user-mgmt/InnerSidebarElement.astro +111 -104
- package/frontend/components/dashboard/user-mgmt/UserListItem.astro +9 -22
- package/frontend/components/dashboard/user-mgmt/UserListItems.astro +18 -0
- package/frontend/components/first-time-setup/snippets/{opt2-studiocms.config.diff → studiocms.config.diff} +1 -0
- package/frontend/components/shared/Code.astro +1 -4
- package/frontend/components/shared/DynamicSettingsRenderer.astro +1 -1
- package/frontend/components/shared/SSRUser.astro +2 -4
- package/frontend/components/shared/foldertree/FolderTreeNode.astro +0 -6
- package/frontend/components/shared/storage-manager/StorageCopyOutput.astro +0 -1
- package/frontend/components/shared/taxonomy/TaxonomyTreeNode.astro +0 -6
- package/frontend/layouts/DashboardLayout.astro +1 -10
- package/frontend/layouts/TaxonomyLayout.astro +0 -1
- package/frontend/middleware/index.ts +102 -61
- package/frontend/pages/404.astro +5 -9
- package/frontend/pages/[dashboard]/[...pluginPage].astro +10 -1
- package/frontend/pages/[dashboard]/configuration.astro +10 -1
- package/frontend/pages/[dashboard]/content-management/createfolder.astro +10 -1
- package/frontend/pages/[dashboard]/content-management/createpage.astro +10 -1
- package/frontend/pages/[dashboard]/content-management/diff.astro +39 -14
- package/frontend/pages/[dashboard]/content-management/editfolder.astro +10 -1
- package/frontend/pages/[dashboard]/content-management/editpage.astro +10 -1
- package/frontend/pages/[dashboard]/content-management/index.astro +10 -1
- package/frontend/pages/[dashboard]/index.astro +10 -1
- package/frontend/pages/[dashboard]/login.astro +86 -25
- package/frontend/pages/[dashboard]/password-reset.astro +22 -16
- package/frontend/pages/[dashboard]/plugins/[plugin].astro +10 -1
- package/frontend/pages/[dashboard]/profile.astro +10 -1
- package/frontend/pages/[dashboard]/signup.astro +153 -52
- package/frontend/pages/[dashboard]/smtp-configuration.astro +77 -75
- package/frontend/pages/[dashboard]/system-management.astro +10 -1
- package/frontend/pages/[dashboard]/taxonomy/categories.astro +30 -41
- package/frontend/pages/[dashboard]/taxonomy/index.astro +10 -0
- package/frontend/pages/[dashboard]/taxonomy/tags.astro +33 -43
- package/frontend/pages/[dashboard]/unverified-email.astro +29 -21
- package/frontend/pages/[dashboard]/user-management/edit.astro +170 -90
- package/frontend/pages/[dashboard]/user-management/index.astro +10 -1
- package/frontend/pages/studiocms_api/[...all].ts +106 -0
- package/frontend/pages/studiocms_api/_handlers/_utils/auth.ts +26 -0
- package/frontend/pages/studiocms_api/_handlers/_utils/changelog.ts +147 -0
- package/frontend/pages/studiocms_api/_handlers/_utils/db-studio-driver.ts +46 -0
- package/frontend/pages/studiocms_api/_handlers/_utils/parseLogLevel.ts +27 -0
- package/frontend/pages/studiocms_api/_handlers/auth/auth.ts +459 -0
- package/frontend/pages/studiocms_api/_handlers/auth/index.ts +17 -0
- package/frontend/pages/studiocms_api/_handlers/auth/oauth.ts +91 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/_shared.ts +57 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/apiTokens.ts +134 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/config.ts +64 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/content.ts +741 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/create.ts +480 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/emailNotifications.ts +49 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/index.ts +45 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/mailer.ts +136 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/plugins.ts +80 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/profile.ts +275 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/resetPassword.ts +140 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/search.ts +63 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/taxonomy.ts +285 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/templates.ts +75 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/users.ts +312 -0
- package/frontend/pages/studiocms_api/_handlers/dashboard/verifyEndpoints.ts +307 -0
- package/frontend/pages/studiocms_api/_handlers/integration/dbStudio.ts +98 -0
- package/frontend/pages/studiocms_api/_handlers/integration/index.ts +17 -0
- package/frontend/pages/studiocms_api/_handlers/integration/storageManager.ts +107 -0
- package/frontend/pages/studiocms_api/_handlers/rest-api/index.ts +8 -0
- package/frontend/pages/studiocms_api/_handlers/rest-api/v1/_shared.ts +41 -0
- package/frontend/pages/studiocms_api/_handlers/rest-api/v1/index.ts +17 -0
- package/frontend/pages/studiocms_api/_handlers/rest-api/v1/public.ts +195 -0
- package/frontend/pages/studiocms_api/_handlers/rest-api/v1/secure.ts +1726 -0
- package/frontend/pages/studiocms_api/_handlers/sdk.ts +129 -0
- package/frontend/pages/studiocms_api/_middleware/astroLocals.ts +36 -0
- package/frontend/pages/studiocms_api/_middleware/restApi.ts +56 -0
- package/frontend/pages/studiocms_api/integrations/[...all].ts +8 -0
- package/frontend/scripts/formdata.ts +219 -0
- package/frontend/setup-pages/3-done.astro +4 -20
- package/frontend/setup-pages/studiocms_api/dashboard/step-2.ts +3 -6
- package/frontend/styles/dashboard-base.css +0 -1
- package/frontend/web-vitals/endpoint.ts +2 -1
- package/package.json +35 -31
- package/dist/global.d.ts +0 -9
- package/frontend/components/dashboard/LoginChecker.astro +0 -68
- package/frontend/components/dashboard/user-mgmt/RankCheck.astro +0 -57
- package/frontend/components/first-time-setup/snippets/opt1-astro.config.diff +0 -14
- package/frontend/components/first-time-setup/snippets/opt2-astro.config.diff +0 -9
- package/frontend/middleware/_authmap.ts +0 -63
- package/frontend/pages/studiocms_api/auth/[path].ts +0 -390
- package/frontend/pages/studiocms_api/auth/[provider]/[...id].ts +0 -64
- package/frontend/pages/studiocms_api/auth/[provider]/_effects/index.ts +0 -101
- package/frontend/pages/studiocms_api/auth/_shared.ts +0 -52
- package/frontend/pages/studiocms_api/dashboard/api-tokens.ts +0 -115
- package/frontend/pages/studiocms_api/dashboard/config.ts +0 -74
- package/frontend/pages/studiocms_api/dashboard/content/diff.ts +0 -73
- package/frontend/pages/studiocms_api/dashboard/content/folder.ts +0 -220
- package/frontend/pages/studiocms_api/dashboard/content/page.ts +0 -359
- package/frontend/pages/studiocms_api/dashboard/create-reset-link.ts +0 -77
- package/frontend/pages/studiocms_api/dashboard/create-user-invite.ts +0 -231
- package/frontend/pages/studiocms_api/dashboard/create-user.ts +0 -186
- package/frontend/pages/studiocms_api/dashboard/email-notification-settings-site.ts +0 -74
- package/frontend/pages/studiocms_api/dashboard/mailer/check-email.ts +0 -75
- package/frontend/pages/studiocms_api/dashboard/mailer/config.ts +0 -136
- package/frontend/pages/studiocms_api/dashboard/plugins/[plugin].ts +0 -80
- package/frontend/pages/studiocms_api/dashboard/profile.ts +0 -245
- package/frontend/pages/studiocms_api/dashboard/resend-verify-email.ts +0 -77
- package/frontend/pages/studiocms_api/dashboard/reset-password.ts +0 -124
- package/frontend/pages/studiocms_api/dashboard/search-list.ts +0 -59
- package/frontend/pages/studiocms_api/dashboard/taxonomy-search.ts +0 -47
- package/frontend/pages/studiocms_api/dashboard/taxonomy.ts +0 -230
- package/frontend/pages/studiocms_api/dashboard/templates.ts +0 -74
- package/frontend/pages/studiocms_api/dashboard/update-user-notifications.ts +0 -86
- package/frontend/pages/studiocms_api/dashboard/users.ts +0 -236
- package/frontend/pages/studiocms_api/dashboard/verify-email.ts +0 -83
- package/frontend/pages/studiocms_api/dashboard/verify-session.ts +0 -187
- package/frontend/pages/studiocms_api/integrations/[type]/[...id].ts +0 -15
- package/frontend/pages/studiocms_api/integrations/[type]/_routes/db-studio.ts +0 -173
- package/frontend/pages/studiocms_api/integrations/[type]/_routes/storage.ts +0 -88
- package/frontend/pages/studiocms_api/partials/editor.astro +0 -74
- package/frontend/pages/studiocms_api/partials/render.astro +0 -39
- package/frontend/pages/studiocms_api/partials/user-list-items.astro +0 -43
- package/frontend/pages/studiocms_api/rest/utils/auth-token.ts +0 -59
- package/frontend/pages/studiocms_api/rest/v1/[type]/[...id].ts +0 -23
- package/frontend/pages/studiocms_api/rest/v1/[type]/_routes/categories.ts +0 -267
- package/frontend/pages/studiocms_api/rest/v1/[type]/_routes/folders.ts +0 -283
- package/frontend/pages/studiocms_api/rest/v1/[type]/_routes/pages.ts +0 -505
- package/frontend/pages/studiocms_api/rest/v1/[type]/_routes/settings.ts +0 -100
- package/frontend/pages/studiocms_api/rest/v1/[type]/_routes/tags.ts +0 -229
- package/frontend/pages/studiocms_api/rest/v1/[type]/_routes/users.ts +0 -553
- package/frontend/pages/studiocms_api/rest/v1/public/[type]/[...id].ts +0 -19
- package/frontend/pages/studiocms_api/rest/v1/public/[type]/_routes/categories.ts +0 -74
- package/frontend/pages/studiocms_api/rest/v1/public/[type]/_routes/folders.ts +0 -85
- package/frontend/pages/studiocms_api/rest/v1/public/[type]/_routes/pages.ts +0 -103
- package/frontend/pages/studiocms_api/rest/v1/public/[type]/_routes/tags.ts +0 -67
- package/frontend/pages/studiocms_api/sdk/[...path].ts +0 -97
- package/frontend/pages/studiocms_api/sdk/utils/changelog.ts +0 -119
- package/frontend/scripts/auth/formListener.ts +0 -81
- package/frontend/scripts/formdata-utils.ts +0 -116
- package/frontend/utils/build-partial-schema.ts +0 -46
- package/frontend/utils/errors.ts +0 -6
- package/frontend/utils/param-extractor.ts +0 -83
- 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));
|