VYPR
High severityNVD Advisory· Published Mar 10, 2026· Updated Mar 10, 2026

StudioCMS Affected by Privilege Escalation via Insecure API Token Generation

CVE-2026-30944

Description

StudioCMS is a server-side-rendered, Astro native, headless content management system. Prior to 0.4.0, the /studiocms_api/dashboard/api-tokens endpoint allows any authenticated user (at least Editor) to generate API tokens for any other user, including owner and admin accounts. The endpoint fails to validate whether the requesting user is authorized to create tokens on behalf of the target user ID, resulting in a full privilege escalation. This vulnerability is fixed in 0.4.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
studiocmsnpm
< 0.4.00.4.0

Affected products

1

Patches

2
9eec9c3b4552

feat(api): add admin API token revocation endpoint and correct permission ranks order (#1440)

https://github.com/withstudiocms/studiocmsAdam MatthiesenMar 7, 2026via ghsa
11 files changed · +169 114
  • .changeset/eighty-forks-attend.md+6 0 added
    @@ -0,0 +1,6 @@
    +---
    +"@withstudiocms/api-spec": minor
    +"studiocms": minor
    +---
    +
    +Introduces new admin level api token revocation endpoint
    
  • .changeset/sweet-jars-remain.md+5 0 added
    @@ -0,0 +1,5 @@
    +---
    +"@withstudiocms/auth-kit": patch
    +---
    +
    +Corrects permission ranks order
    
  • packages/studiocms/frontend/pages/[dashboard]/user-management/edit.astro+44 54 modified
    @@ -530,61 +530,51 @@ setDataContext(Astro, {
             button.addEventListener('click', async (event) => {
                 event.preventDefault();
     
    -            // TODO: Implement Admin level API token revocation that allows revoking any token, not just tokens owned by the user. This will require additional permission checks to ensure only Admin users can revoke tokens that they do not own.
    -
    -            toast({
    -              title: 'Error',
    -              description: 'Not Currently implemented',
    -              type: 'danger',
    -              duration: 5000,
    -            });
    -            return;
    -
    -            // const tokenID = button.getAttribute('data-token');
    -            // const userID = button.getAttribute('data-user');
    +            const tokenID = button.getAttribute('data-token');
    +            const userID = button.getAttribute('data-user');
                 
    -            // if (!tokenID || !userID) {
    -            //     toast({
    -            //         title: 'Error',
    -            //         description: 'Missing necessary data to revoke API token.',
    -            //         type: 'danger',
    -            //         duration: 5000
    -            //     })
    -            //     return;
    -            // }
    -
    -            // const response = await dashboardClient.pipe(
    -            //   Effect.flatMap((client) =>
    -            //     client.apiTokens.revokeApiToken({
    -            //       payload: {
    -            //         tokenID,
    -            //         userID
    -            //       }
    -            //     })
    -            //   ),
    -            //   Effect.catchTags(dashboardSharedCatchTags),
    -            //   Effect.runPromise
    -            // );
    -
    -            // if ('error' in response) {
    -            //   toast({
    -            //     title: 'Error',
    -            //     description: response.error,
    -            //     type: 'danger',
    -            //     duration: 5000
    -            //   })
    -            //   return;
    -            // } else {
    -            //   toast({
    -            //     title: 'Success',
    -            //     description: response.message || 'API token revoked successfully.',
    -            //     type: 'success',
    -            //     duration: 5000
    -            //   })
    -            //   setTimeout(() => {
    -            //     window.location.reload();
    -            //   }, 1000);
    -            // }
    +            if (!tokenID || !userID) {
    +                toast({
    +                    title: 'Error',
    +                    description: 'Missing necessary data to revoke API token.',
    +                    type: 'danger',
    +                    duration: 5000
    +                })
    +                return;
    +            }
    +
    +            const response = await dashboardClient.pipe(
    +              Effect.flatMap((client) =>
    +                client.apiTokens.adminRevokeUserApiToken({
    +                  payload: {
    +                    tokenID,
    +                    userID
    +                  }
    +                })
    +              ),
    +              Effect.catchTags(dashboardSharedCatchTags),
    +              Effect.runPromise
    +            );
    +
    +            if ('error' in response) {
    +              toast({
    +                title: 'Error',
    +                description: response.error,
    +                type: 'danger',
    +                duration: 5000
    +              })
    +              return;
    +            } else {
    +              toast({
    +                title: 'Success',
    +                description: response.message || 'API token revoked successfully.',
    +                type: 'success',
    +                duration: 5000
    +              })
    +              setTimeout(() => {
    +                window.location.reload();
    +              }, 1000);
    +            }
             });
         });
       </script>
    
  • packages/studiocms/frontend/pages/studiocms_api/_handlers/dashboard/apiTokens.ts+44 2 modified
    @@ -5,11 +5,10 @@ import { HttpApiBuilder } from '@effect/platform';
     import { StudioCMSDashboardApiSpec } from '@withstudiocms/api-spec';
     import { CurrentUser } from '@withstudiocms/api-spec/astro-context';
     import { DashboardAPIError } from '@withstudiocms/api-spec/dashboard';
    +import { availablePermissionRanks } from '@withstudiocms/auth-kit/types';
     import { Effect } from 'effect';
     import { sharedDBErrors } from './_shared.js';
     
    -// TODO: Implement Admin level API token revocation that allows revoking any token, not just tokens owned by the user. This will require additional permission checks to ensure only Admin users can revoke tokens that they do not own.
    -
     /**
      * Check if the Dashboard API is enabled in the route configuration.
      */
    @@ -84,6 +83,49 @@ export const ApiTokensHandler = HttpApiBuilder.group(
     
     					yield* sdk.REST_API.tokens.delete({ tokenId: tokenID, userId: userData.user.id });
     
    +					return {
    +						message: 'Token deleted',
    +					};
    +				}, Effect.catchTags(sharedDBErrors))
    +			)
    +			.handle(
    +				'adminRevokeUserApiToken',
    +				Effect.fn(function* ({ payload: { tokenID, userID } }) {
    +					if (!dashboardAPIEnabled) {
    +						return yield* new DashboardAPIError({ error: 'Dashboard API is disabled' });
    +					}
    +
    +					if (developerConfig.demoMode !== false) {
    +						return yield* new DashboardAPIError({
    +							error: 'Demo mode is enabled, this action is not allowed.',
    +						});
    +					}
    +
    +					const [sdk, userData] = yield* Effect.all([SDKCore, CurrentUser]);
    +
    +					const isAuthorized = userData.userPermissionLevel.isAdmin;
    +
    +					if (!userData.isLoggedIn || !isAuthorized) {
    +						return yield* new DashboardAPIError({ error: 'Unauthorized' });
    +					}
    +
    +					const tokenData = yield* sdk.REST_API.tokens.verify(tokenID);
    +
    +					if (!tokenData) {
    +						return yield* new DashboardAPIError({ error: 'Token not found' });
    +					}
    +
    +					const targetPerms = availablePermissionRanks.indexOf(tokenData.rank);
    +					const userPerms = availablePermissionRanks.indexOf(userData.permissionLevel);
    +
    +					if (targetPerms >= userPerms) {
    +						return yield* new DashboardAPIError({
    +							error: 'Unauthorized - insufficient permissions to revoke this token',
    +						});
    +					}
    +
    +					yield* sdk.REST_API.tokens.delete({ tokenId: tokenID, userId: userID });
    +
     					return {
     						message: 'Token deleted',
     					};
    
  • packages/studiocms/frontend/pages/studiocms_api/_handlers/dashboard/create.ts+10 16 modified
    @@ -9,7 +9,7 @@ import { HttpApiBuilder } from '@effect/platform';
     import { StudioCMSDashboardApiSpec } from '@withstudiocms/api-spec';
     import { AstroAPIContext, CurrentUser } from '@withstudiocms/api-spec/astro-context';
     import { DashboardAPIError } from '@withstudiocms/api-spec/dashboard';
    -import type { AvailablePermissionRanks } from '@withstudiocms/auth-kit/types';
    +import { availablePermissionRanks } from '@withstudiocms/auth-kit/types';
     import { appendSearchParamsToUrl } from '@withstudiocms/effect/effect';
     import type { APIContext } from 'astro';
     import { Effect, pipe } from 'effect';
    @@ -22,17 +22,6 @@ import { sharedDBErrors, sharedNotifierErrors } from './_shared.js';
      */
     const dashboardAPIEnabled = routeConfig.dashboardAPIEnabled;
     
    -/**
    - * Array of available permission levels for the REST API. This is used to check user permissions when accessing secure endpoints, ensuring that only users with the appropriate rank can perform certain actions. The ranks are defined in ascending order of permissions, with 'unknown' having the least permissions and 'owner' having the most.
    - */
    -const permissionLevels: AvailablePermissionRanks[] = [
    -	'unknown',
    -	'visitor',
    -	'editor',
    -	'admin',
    -	'owner',
    -];
    -
     /**
      * Type definition for the token object returned when creating a password reset link. This includes the token ID, the user ID it is associated with, and the token string itself. This type is used to ensure that the correct data structure is returned and handled when generating password reset links for users.
      */
    @@ -188,8 +177,8 @@ export const CreateHandlers = HttpApiBuilder.group(
     							return yield* new DashboardAPIError({ error: 'Invalid rank' });
     						}
     
    -						const callerPerm = permissionLevels.indexOf(userData.permissionLevel);
    -						const targetPerm = permissionLevels.indexOf(rank);
    +						const callerPerm = availablePermissionRanks.indexOf(userData.permissionLevel);
    +						const targetPerm = availablePermissionRanks.indexOf(rank);
     
     						if (targetPerm >= callerPerm) {
     							return yield* new DashboardAPIError({
    @@ -325,9 +314,14 @@ export const CreateHandlers = HttpApiBuilder.group(
     
     						const { username, email, displayname, rank, originalUrl } = payload;
     
    -						const userPerms = userData.userPermissionLevel;
    +						if (!ValidRanks.has(rank) || rank === 'unknown') {
    +							return yield* new DashboardAPIError({ error: 'Invalid rank' });
    +						}
    +
    +						const callerPerm = availablePermissionRanks.indexOf(userData.permissionLevel);
    +						const targetPerm = availablePermissionRanks.indexOf(rank);
     
    -						if (rank === 'owner' && !userPerms?.isOwner) {
    +						if (targetPerm >= callerPerm) {
     							return yield* new DashboardAPIError({
     								error: 'Unauthorized: insufficient permissions to assign target rank',
     							});
    
  • packages/studiocms/frontend/pages/studiocms_api/_handlers/dashboard/users.ts+6 17 modified
    @@ -7,7 +7,7 @@ import { HttpApiBuilder } from '@effect/platform';
     import { StudioCMSDashboardApiSpec } from '@withstudiocms/api-spec';
     import { CurrentUser } from '@withstudiocms/api-spec/astro-context';
     import { DashboardAPIError } from '@withstudiocms/api-spec/dashboard';
    -import type { AvailablePermissionRanks } from '@withstudiocms/auth-kit/types';
    +import { availablePermissionRanks } from '@withstudiocms/auth-kit/types';
     import { Effect } from 'effect';
     import { ValidRanks } from '#consts';
     import { sharedDBErrors, sharedNotifierErrors } from './_shared.js';
    @@ -17,17 +17,6 @@ import { sharedDBErrors, sharedNotifierErrors } from './_shared.js';
      */
     const dashboardAPIEnabled = routeConfig.dashboardAPIEnabled;
     
    -/**
    - * Array of available permission levels for the REST API. This is used to check user permissions when accessing secure endpoints, ensuring that only users with the appropriate rank can perform certain actions. The ranks are defined in ascending order of permissions, with 'unknown' having the least permissions and 'owner' having the most.
    - */
    -const permissionLevels: AvailablePermissionRanks[] = [
    -	'unknown',
    -	'visitor',
    -	'editor',
    -	'admin',
    -	'owner',
    -];
    -
     /**
      * Users Handlers for the Dashboard API
      */
    @@ -80,11 +69,11 @@ export const UsersHandlers = HttpApiBuilder.group(StudioCMSDashboardApiSpec, 'us
     						});
     					}
     
    -					const userPerms = permissionLevels.indexOf(userData.permissionLevel);
    -					const targetCurrentLevel = permissionLevels.indexOf(
    +					const userPerms = availablePermissionRanks.indexOf(userData.permissionLevel);
    +					const targetCurrentLevel = availablePermissionRanks.indexOf(
     						user.permissionsData?.rank || 'unknown'
     					);
    -					const targetNewLevel = permissionLevels.indexOf(rank);
    +					const targetNewLevel = availablePermissionRanks.indexOf(rank);
     
     					if (userPerms === -1 || targetCurrentLevel === -1 || targetNewLevel === -1) {
     						return yield* new DashboardAPIError({
    @@ -203,8 +192,8 @@ export const UsersHandlers = HttpApiBuilder.group(StudioCMSDashboardApiSpec, 'us
     						});
     					}
     
    -					const actorPerms = permissionLevels.indexOf(userData.permissionLevel);
    -					const targetPerms = permissionLevels.indexOf(
    +					const actorPerms = availablePermissionRanks.indexOf(userData.permissionLevel);
    +					const targetPerms = availablePermissionRanks.indexOf(
     						targetUser.permissionsData?.rank || 'unknown'
     					);
     
    
  • packages/studiocms/frontend/pages/studiocms_api/_handlers/rest-api/v1/secure.ts+13 21 modified
    @@ -10,7 +10,10 @@ import {
     	CurrentRestAPIUser,
     	RestAPIError,
     } from '@withstudiocms/api-spec/rest-api';
    -import type { AvailablePermissionRanks } from '@withstudiocms/auth-kit/types';
    +import {
    +	type AvailablePermissionRanks,
    +	availablePermissionRanks,
    +} from '@withstudiocms/auth-kit/types';
     import {
     	StudioCMSPageData,
     	StudioCMSPageDataCategories,
    @@ -38,17 +41,6 @@ const StringArrayCodec = Schema.transform(Schema.String, Schema.Array(Schema.Str
      */
     const encodeStringArray = Schema.encode(StringArrayCodec);
     
    -/**
    - * Array of available permission levels for the REST API. This is used to check user permissions when accessing secure endpoints, ensuring that only users with the appropriate rank can perform certain actions. The ranks are defined in ascending order of permissions, with 'unknown' having the least permissions and 'owner' having the most.
    - */
    -const permissionLevels: AvailablePermissionRanks[] = [
    -	'unknown',
    -	'visitor',
    -	'editor',
    -	'admin',
    -	'owner',
    -];
    -
     /**
      * REST API v1 Secure Handler
      *
    @@ -1488,8 +1480,8 @@ export const RestApiSecureHandler = HttpApiBuilder.group(
     						const { permissionsData } = existingUser;
     
     						const existingUserRank = permissionsData?.rank ?? 'unknown';
    -						const existingUserRankIndex = permissionLevels.indexOf(existingUserRank);
    -						const loggedInUserRankIndex = permissionLevels.indexOf(user.rank);
    +						const existingUserRankIndex = availablePermissionRanks.indexOf(existingUserRank);
    +						const loggedInUserRankIndex = availablePermissionRanks.indexOf(user.rank);
     
     						if (loggedInUserRankIndex <= existingUserRankIndex) {
     							return yield* new RestAPIError({
    @@ -1556,17 +1548,17 @@ export const RestApiSecureHandler = HttpApiBuilder.group(
     						const { permissionsData } = existingUser;
     
     						const existingUserRank = permissionsData?.rank ?? 'unknown';
    -						const existingUserRankIndex = permissionLevels.indexOf(existingUserRank);
    -						const loggedInUserRankIndex = permissionLevels.indexOf(user.rank);
    +						const existingUserRankIndex = availablePermissionRanks.indexOf(existingUserRank);
    +						const loggedInUserRankIndex = availablePermissionRanks.indexOf(user.rank);
     
    -						if (loggedInUserRankIndex < existingUserRankIndex) {
    +						if (loggedInUserRankIndex <= existingUserRankIndex) {
     							return yield* new RestAPIError({
     								error: 'Unauthorized to update user with higher rank',
     							});
     						}
     
     						if (payload.rank) {
    -							const payloadRankIndex = permissionLevels.indexOf(payload.rank);
    +							const payloadRankIndex = availablePermissionRanks.indexOf(payload.rank);
     
     							if (loggedInUserRankIndex <= payloadRankIndex) {
     								return yield* new RestAPIError({
    @@ -1712,10 +1704,10 @@ export const RestApiSecureHandler = HttpApiBuilder.group(
     							username,
     						};
     
    -						const existingUserRankIndex = permissionLevels.indexOf(existingUserRank);
    -						const loggedInUserRankIndex = permissionLevels.indexOf(user.rank);
    +						const existingUserRankIndex = availablePermissionRanks.indexOf(existingUserRank);
    +						const loggedInUserRankIndex = availablePermissionRanks.indexOf(user.rank);
     
    -						if (loggedInUserRankIndex < existingUserRankIndex) {
    +						if (loggedInUserRankIndex <= existingUserRankIndex) {
     							return yield* new RestAPIError({
     								error: 'Unauthorized to view user with higher rank',
     							});
    
  • packages/@withstudiocms/api-spec/src/dashboard/index.ts+2 1 modified
    @@ -3,7 +3,7 @@ import { Description, License, Title, Transform, Version } from '@effect/platfor
     import pkg from '../../package.json';
     import { StudioCMSLicenseAnnotation, StudioCMSTransformAnnotation } from '../consts.js';
     import { DashboardAPIError } from './errors.js';
    -import { apiTokensDelete, apiTokensPost } from './routes/api-tokens.js';
    +import { adminApiTokensDelete, apiTokensDelete, apiTokensPost } from './routes/api-tokens.js';
     import { configPost } from './routes/config.js';
     import {
     	contentDiffPost,
    @@ -44,6 +44,7 @@ export class DashboardApiTokensGroup extends HttpApiGroup.make('apiTokens')
     	.annotate(License, StudioCMSLicenseAnnotation)
     	.add(apiTokensPost)
     	.add(apiTokensDelete)
    +	.add(adminApiTokensDelete)
     	.addError(DashboardAPIError, { status: 500 })
     	.prefix('/dashboard') {}
     
    
  • packages/@withstudiocms/api-spec/src/dashboard/routes/api-tokens.ts+26 0 modified
    @@ -3,6 +3,7 @@ import { Description, Summary, Title } from '@effect/platform/OpenApi';
     import { AstroLocalsMiddleware } from '../../astro-context.js';
     import { DashboardAPIError } from '../errors.js';
     import {
    +	AdminDeleteApiTokenPayload,
     	CreateApiTokenPayload,
     	CreateApiTokenResponse,
     	DeleteApiTokenPayload,
    @@ -54,3 +55,28 @@ export const apiTokensDelete = HttpApiEndpoint.del('revokeApiToken', '/api-token
     	.addError(DashboardAPIError, { status: 400 })
     	.addError(DashboardAPIError, { status: 403 })
     	.addError(DashboardAPIError, { status: 500 });
    +
    +/**
    + * Admin Revoke User API Token Endpoint
    + *
    + * This endpoint allows Admin users to revoke any user's API token for the StudioCMS Dashboard API.
    + */
    +export const adminApiTokensDelete = HttpApiEndpoint.del(
    +	'adminRevokeUserApiToken',
    +	'/api-tokens/admin'
    +)
    +	.annotate(Title, 'Admin Revoke User API Token')
    +	.annotate(
    +		Summary,
    +		"Admin level endpoint to revoke any user's API token for the StudioCMS Dashboard API"
    +	)
    +	.annotate(
    +		Description,
    +		"Admin level endpoint that allows revoking any user's API token for the StudioCMS Dashboard API. This endpoint requires additional permission checks to ensure only Admin users can revoke tokens that they do not own.\n\n> [!note]\n> This endpoint verifies User authentication using [Astro Locals Context](https://docs.astro.build/en/guides/middleware/#storing-data-in-contextlocals) and requires users to be logged into the current StudioCMS instance with Admin level permissions."
    +	)
    +	.middleware(AstroLocalsMiddleware)
    +	.setPayload(AdminDeleteApiTokenPayload)
    +	.addSuccess(successResponseSchema)
    +	.addError(DashboardAPIError, { status: 400 })
    +	.addError(DashboardAPIError, { status: 403 })
    +	.addError(DashboardAPIError, { status: 500 });
    
  • packages/@withstudiocms/api-spec/src/dashboard/schemas.ts+12 2 modified
    @@ -7,8 +7,6 @@ import {
     import * as Schema from 'effect/Schema';
     import { StudioCMSDynamicSiteConfigData } from '../rest-api/schemas.js';
     
    -// TODO: Implement Admin level API token revocation that allows revoking any token, not just tokens owned by the user. This will require additional permission checks to ensure only Admin users can revoke tokens that they do not own.
    -
     /**
      * Standard error response schema for the StudioCMS Dashboard API.
      *
    @@ -296,6 +294,18 @@ export const DeleteApiTokenPayload = Schema.Struct({
     	description: 'Payload schema for deleting an existing API token in the StudioCMS Dashboard API.',
     });
     
    +/**
    + * Payload schema for Admin users to delete any user's API token in the StudioCMS Dashboard API.
    + */
    +export const AdminDeleteApiTokenPayload = Schema.Struct({
    +	...DeleteApiTokenPayload.fields,
    +	userID: Schema.String,
    +}).annotations({
    +	title: 'Admin Delete API Token Payload',
    +	description:
    +		"Payload schema for Admin users to delete any user's API token in the StudioCMS Dashboard API. This includes the token ID of the token to be revoked and the user ID of the token owner.",
    +});
    +
     /**
      * Payload schema for updating the dynamic site configuration in the StudioCMS Dashboard API.
      *
    
  • packages/@withstudiocms/auth-kit/src/types.ts+1 1 modified
    @@ -92,7 +92,7 @@ export interface OAuthData {
      * - 'visitor': A low-level permission, for users who can view content but not modify it.
      * - 'unknown': A default or fallback permission rank for users with undefined roles.
      */
    -export const availablePermissionRanks = ['owner', 'admin', 'editor', 'visitor', 'unknown'] as const;
    +export const availablePermissionRanks = ['unknown', 'visitor', 'editor', 'admin', 'owner'] as const;
     
     /**
      * Represents the available permission ranks for a user.
    
f4a209fc090c

refactor(api): clean up API handlers and remove unnecessary variables (#1438)

https://github.com/withstudiocms/studiocmsAdam MatthiesenMar 7, 2026via ghsa
8 files changed · +138 161
  • .changeset/fair-files-notice.md+6 0 added
    @@ -0,0 +1,6 @@
    +---
    +"@withstudiocms/api-spec": minor
    +"studiocms": minor
    +---
    +
    +Refactors and cleans up API spec handlers to remove un-needed variables and tighten security
    
  • packages/studiocms/frontend/components/dashboard/profile/APITokens.astro+1 3 modified
    @@ -240,7 +240,6 @@ const t = useTranslations(lang, '@studiocms/dashboard:profile');
                     client.apiTokens.createApiToken({
                         payload: {
                             description,
    -                        user,
                         },
                     }),
                 ),
    @@ -285,8 +284,7 @@ const t = useTranslations(lang, '@studiocms/dashboard:profile');
                     Effect.flatMap((client) => 
                         client.apiTokens.revokeApiToken({
                             payload: {
    -                            tokenID,
    -                            userID
    +                            tokenID
                             }
                         })
                     ),
    
  • packages/studiocms/frontend/pages/[dashboard]/user-management/edit.astro+55 44 modified
    @@ -529,51 +529,62 @@ setDataContext(Astro, {
         deleteButtons.forEach((button) => {
             button.addEventListener('click', async (event) => {
                 event.preventDefault();
    -            const tokenID = button.getAttribute('data-token');
    -            const userID = button.getAttribute('data-user');
    -
    -            if (!tokenID || !userID) {
    -                toast({
    -                    title: 'Error',
    -                    description: 'Missing necessary data to revoke API token.',
    -                    type: 'danger',
    -                    duration: 5000
    -                })
    -                return;
    -            }
     
    -            const response = await dashboardClient.pipe(
    -              Effect.flatMap((client) =>
    -                client.apiTokens.revokeApiToken({
    -                  payload: {
    -                    tokenID,
    -                    userID
    -                  }
    -                })
    -              ),
    -              Effect.catchTags(dashboardSharedCatchTags),
    -              Effect.runPromise
    -            );
    -
    -            if ('error' in response) {
    -              toast({
    -                title: 'Error',
    -                description: response.error,
    -                type: 'danger',
    -                duration: 5000
    -              })
    -              return;
    -            } else {
    -              toast({
    -                title: 'Success',
    -                description: response.message || 'API token revoked successfully.',
    -                type: 'success',
    -                duration: 5000
    -              })
    -              setTimeout(() => {
    -                window.location.reload();
    -              }, 1000);
    -            }
    +            // TODO: Implement Admin level API token revocation that allows revoking any token, not just tokens owned by the user. This will require additional permission checks to ensure only Admin users can revoke tokens that they do not own.
    +
    +            toast({
    +              title: 'Error',
    +              description: 'Not Currently implemented',
    +              type: 'danger',
    +              duration: 5000,
    +            });
    +            return;
    +
    +            // const tokenID = button.getAttribute('data-token');
    +            // const userID = button.getAttribute('data-user');
    +            
    +            // if (!tokenID || !userID) {
    +            //     toast({
    +            //         title: 'Error',
    +            //         description: 'Missing necessary data to revoke API token.',
    +            //         type: 'danger',
    +            //         duration: 5000
    +            //     })
    +            //     return;
    +            // }
    +
    +            // const response = await dashboardClient.pipe(
    +            //   Effect.flatMap((client) =>
    +            //     client.apiTokens.revokeApiToken({
    +            //       payload: {
    +            //         tokenID,
    +            //         userID
    +            //       }
    +            //     })
    +            //   ),
    +            //   Effect.catchTags(dashboardSharedCatchTags),
    +            //   Effect.runPromise
    +            // );
    +
    +            // if ('error' in response) {
    +            //   toast({
    +            //     title: 'Error',
    +            //     description: response.error,
    +            //     type: 'danger',
    +            //     duration: 5000
    +            //   })
    +            //   return;
    +            // } else {
    +            //   toast({
    +            //     title: 'Success',
    +            //     description: response.message || 'API token revoked successfully.',
    +            //     type: 'success',
    +            //     duration: 5000
    +            //   })
    +            //   setTimeout(() => {
    +            //     window.location.reload();
    +            //   }, 1000);
    +            // }
             });
         });
       </script>
    
  • packages/studiocms/frontend/pages/studiocms_api/_handlers/auth/auth.ts+2 6 modified
    @@ -291,9 +291,7 @@ export const AuthAPIHandler = HttpApiBuilder.group(StudioCMSAuthApi, 'auth', (ha
     				// Provide the necessary dependencies for the login handler
     				Effect.provide(loginRegisterDependencies),
     				// Catch any errors that occur during the login process and return a generic error message to prevent exposing sensitive information about the failure.
    -				Effect.catchTags({
    -					...sharedCatchTags,
    -				})
    +				Effect.catchTags(sharedCatchTags)
     			)
     		)
     		.handle(
    @@ -455,9 +453,7 @@ export const AuthAPIHandler = HttpApiBuilder.group(StudioCMSAuthApi, 'auth', (ha
     				// Provide the necessary dependencies for the login handler
     				Effect.provide(loginRegisterDependencies),
     				// Catch any errors that occur during the login process and return a generic error message to prevent exposing sensitive information about the failure.
    -				Effect.catchTags({
    -					...sharedCatchTags,
    -				})
    +				Effect.catchTags(sharedCatchTags)
     			)
     		)
     );
    
  • packages/studiocms/frontend/pages/studiocms_api/_handlers/dashboard/apiTokens.ts+14 6 modified
    @@ -8,6 +8,8 @@ import { DashboardAPIError } from '@withstudiocms/api-spec/dashboard';
     import { Effect } from 'effect';
     import { sharedDBErrors } from './_shared.js';
     
    +// TODO: Implement Admin level API token revocation that allows revoking any token, not just tokens owned by the user. This will require additional permission checks to ensure only Admin users can revoke tokens that they do not own.
    +
     /**
      * Check if the Dashboard API is enabled in the route configuration.
      */
    @@ -24,7 +26,7 @@ export const ApiTokensHandler = HttpApiBuilder.group(
     			.handle(
     				'createApiToken',
     				Effect.fn(
    -					function* ({ payload: { user, description } }) {
    +					function* ({ payload: { description } }) {
     						if (!dashboardAPIEnabled) {
     							return yield* new DashboardAPIError({ error: 'Dashboard API is disabled' });
     						}
    @@ -39,11 +41,11 @@ export const ApiTokensHandler = HttpApiBuilder.group(
     
     						const isAuthorized = userData.userPermissionLevel.isEditor;
     
    -						if (!userData.isLoggedIn || !isAuthorized) {
    +						if (!userData.isLoggedIn || !userData.user || !isAuthorized) {
     							return yield* new DashboardAPIError({ error: 'Unauthorized' });
     						}
     
    -						const newToken = yield* sdk.REST_API.tokens.new(user, description);
    +						const newToken = yield* sdk.REST_API.tokens.new(userData.user.id, description);
     
     						return { token: newToken.key };
     					},
    @@ -55,7 +57,7 @@ export const ApiTokensHandler = HttpApiBuilder.group(
     			)
     			.handle(
     				'revokeApiToken',
    -				Effect.fn(function* ({ payload: { tokenID, userID } }) {
    +				Effect.fn(function* ({ payload: { tokenID } }) {
     					if (!dashboardAPIEnabled) {
     						return yield* new DashboardAPIError({ error: 'Dashboard API is disabled' });
     					}
    @@ -70,11 +72,17 @@ export const ApiTokensHandler = HttpApiBuilder.group(
     
     					const isAuthorized = userData.userPermissionLevel.isEditor;
     
    -					if (!userData.isLoggedIn || !isAuthorized) {
    +					if (!userData.isLoggedIn || !userData.user || !isAuthorized) {
    +						return yield* new DashboardAPIError({ error: 'Unauthorized' });
    +					}
    +
    +					const tokenData = yield* sdk.REST_API.tokens.verify(tokenID);
    +
    +					if (!tokenData || tokenData.userId !== userData.user.id) {
     						return yield* new DashboardAPIError({ error: 'Unauthorized' });
     					}
     
    -					yield* sdk.REST_API.tokens.delete({ tokenId: tokenID, userId: userID });
    +					yield* sdk.REST_API.tokens.delete({ tokenId: tokenID, userId: userData.user.id });
     
     					return {
     						message: 'Token deleted',
    
  • packages/studiocms/frontend/pages/studiocms_api/_handlers/dashboard/mailer.ts+55 91 modified
    @@ -4,15 +4,67 @@ import routeConfig from 'virtual:studiocms/route-config';
     import { HttpApiBuilder } from '@effect/platform';
     import { StudioCMSDashboardApiSpec } from '@withstudiocms/api-spec';
     import { CurrentUser } from '@withstudiocms/api-spec/astro-context';
    -import { DashboardAPIError } from '@withstudiocms/api-spec/dashboard';
    +import { DashboardAPIError, type MailerSmtpConfigPayload } from '@withstudiocms/api-spec/dashboard';
     import { Effect } from 'effect';
    +import type { DynamicHttpApiHandlerParams } from 'effectify/httpApi';
     import { sharedDBErrors, sharedNotifierErrors } from './_shared.js';
     
     /**
      * Check if the Dashboard API is enabled in the route configuration.
      */
     const dashboardAPIEnabled = routeConfig.dashboardAPIEnabled;
     
    +/**
    + * Shared effect for handling both setup and update mailer configuration endpoints since they have the same logic and requirements.
    + */
    +const mailerEffect = (type: 'setup' | 'update') =>
    +	Effect.fn(
    +		function* ({
    +			payload,
    +		}: DynamicHttpApiHandlerParams<{
    +			payloadSchema: typeof MailerSmtpConfigPayload.Type;
    +		}>) {
    +			if (!dashboardAPIEnabled) {
    +				return yield* new DashboardAPIError({
    +					error: 'Dashboard API is disabled',
    +				});
    +			}
    +
    +			if (developerConfig.demoMode !== false) {
    +				return yield* new DashboardAPIError({
    +					error: 'Demo mode is enabled, this action is not allowed.',
    +				});
    +			}
    +
    +			const [mailer, userData] = yield* Effect.all([Mailer, CurrentUser]);
    +
    +			if (!userData.isLoggedIn || !userData.userPermissionLevel.isOwner) {
    +				return yield* new DashboardAPIError({ error: 'Unauthorized' });
    +			}
    +
    +			if (payload.port && (payload.port < 1 || payload.port > 65535)) {
    +				return yield* new DashboardAPIError({ error: 'Invalid port number' });
    +			}
    +
    +			const config = yield* mailer.createMailerConfigTable(payload);
    +
    +			if (!config) {
    +				return yield* new DashboardAPIError({
    +					error: `Failed to ${type} mailer configuration`,
    +				});
    +			}
    +
    +			return {
    +				message: `Mailer configuration ${type}d successfully`,
    +			};
    +		},
    +		Mailer.Provide,
    +		Effect.catchTags({
    +			...sharedDBErrors,
    +			...sharedNotifierErrors,
    +		})
    +	);
    +
     /**
      * Mailer Handlers for the Dashboard API
      */
    @@ -21,96 +73,8 @@ export const MailerHandlers = HttpApiBuilder.group(
     	'mailer',
     	(handlers) =>
     		handlers
    -			.handle(
    -				'setupMailerConfig',
    -				Effect.fn(
    -					function* ({ payload }) {
    -						if (!dashboardAPIEnabled) {
    -							return yield* new DashboardAPIError({
    -								error: 'Dashboard API is disabled',
    -							});
    -						}
    -
    -						if (developerConfig.demoMode !== false) {
    -							return yield* new DashboardAPIError({
    -								error: 'Demo mode is enabled, this action is not allowed.',
    -							});
    -						}
    -
    -						const [mailer, userData] = yield* Effect.all([Mailer, CurrentUser]);
    -
    -						if (!userData.isLoggedIn || !userData.userPermissionLevel.isOwner) {
    -							return yield* new DashboardAPIError({ error: 'Unauthorized' });
    -						}
    -
    -						if (payload.port && (payload.port < 1 || payload.port > 65535)) {
    -							return yield* new DashboardAPIError({ error: 'Invalid port number' });
    -						}
    -
    -						const config = yield* mailer.createMailerConfigTable(payload);
    -
    -						if (!config) {
    -							return yield* new DashboardAPIError({
    -								error: 'Failed to create mailer configuration',
    -							});
    -						}
    -
    -						return {
    -							message: 'Mailer configuration updated successfully',
    -						};
    -					},
    -					Mailer.Provide,
    -					Effect.catchTags({
    -						...sharedDBErrors,
    -						...sharedNotifierErrors,
    -					})
    -				)
    -			)
    -			.handle(
    -				'updateMailerConfig',
    -				Effect.fn(
    -					function* ({ payload }) {
    -						if (!dashboardAPIEnabled) {
    -							return yield* new DashboardAPIError({
    -								error: 'Dashboard API is disabled',
    -							});
    -						}
    -
    -						if (developerConfig.demoMode !== false) {
    -							return yield* new DashboardAPIError({
    -								error: 'Demo mode is enabled, this action is not allowed.',
    -							});
    -						}
    -
    -						const [mailer, userData] = yield* Effect.all([Mailer, CurrentUser]);
    -
    -						if (!userData.isLoggedIn || !userData.userPermissionLevel.isOwner) {
    -							return yield* new DashboardAPIError({ error: 'Unauthorized' });
    -						}
    -
    -						if (payload.port && (payload.port < 1 || payload.port > 65535)) {
    -							return yield* new DashboardAPIError({ error: 'Invalid port number' });
    -						}
    -
    -						const config = yield* mailer.updateMailerConfigTable(payload);
    -
    -						if (!config) {
    -							return yield* new DashboardAPIError({
    -								error: 'Failed to update mailer configuration',
    -							});
    -						}
    -
    -						return {
    -							message: 'Mailer configuration updated successfully',
    -						};
    -					},
    -					Mailer.Provide,
    -					Effect.catchTags({
    -						...sharedDBErrors,
    -						...sharedNotifierErrors,
    -					})
    -				)
    -			)
    +			.handle('setupMailerConfig', mailerEffect('setup'))
    +			.handle('updateMailerConfig', mailerEffect('update'))
     			.handle(
     				'testEmailService',
     				Effect.fn(
    
  • packages/studiocms/frontend/pages/studiocms_api/_handlers/rest-api/v1/public.ts+3 9 modified
    @@ -4,7 +4,7 @@ import { HttpApiBuilder } from '@effect/platform';
     import { StudioCMSRestApiV1Spec } from '@withstudiocms/api-spec';
     import { RestAPIError } from '@withstudiocms/api-spec/rest-api';
     import { Effect } from 'effect';
    -import { sharedDBErrors } from './_shared.js';
    +import { sharedDBErrors, sharedPageCollectionErrors } from './_shared.js';
     
     const restAPIEnabled = routeConfig.restAPIEnabled;
     
    @@ -124,10 +124,7 @@ export const RestApiPublicHandler = HttpApiBuilder.group(
     						Effect.flatMap(draftMeansFail('Page not found')),
     						Effect.catchTags({
     							...sharedDBErrors,
    -							ParseError: () => new RestAPIError({ error: 'Failed to parse page data' }),
    -							CollectorError: () => new RestAPIError({ error: 'Failed to collect page data' }),
    -							FolderTreeError: () => new RestAPIError({ error: 'Failed to retrieve folder tree' }),
    -							PaginateError: () => new RestAPIError({ error: 'Failed to paginate page data' }),
    +							...sharedPageCollectionErrors,
     						})
     					);
     				})
    @@ -158,10 +155,7 @@ export const RestApiPublicHandler = HttpApiBuilder.group(
     						}),
     						Effect.catchTags({
     							...sharedDBErrors,
    -							ParseError: () => new RestAPIError({ error: 'Failed to parse pages data' }),
    -							CollectorError: () => new RestAPIError({ error: 'Failed to collect pages data' }),
    -							FolderTreeError: () => new RestAPIError({ error: 'Failed to retrieve folder tree' }),
    -							PaginateError: () => new RestAPIError({ error: 'Failed to paginate pages data' }),
    +							...sharedPageCollectionErrors,
     						})
     					);
     				})
    
  • packages/@withstudiocms/api-spec/src/dashboard/schemas.ts+2 2 modified
    @@ -7,6 +7,8 @@ import {
     import * as Schema from 'effect/Schema';
     import { StudioCMSDynamicSiteConfigData } from '../rest-api/schemas.js';
     
    +// TODO: Implement Admin level API token revocation that allows revoking any token, not just tokens owned by the user. This will require additional permission checks to ensure only Admin users can revoke tokens that they do not own.
    +
     /**
      * Standard error response schema for the StudioCMS Dashboard API.
      *
    @@ -262,7 +264,6 @@ export const PluginSettingsPayload = Schema.Record({
      * This schema is used as the request payload when creating a new API token.
      */
     export const CreateApiTokenPayload = Schema.Struct({
    -	user: Schema.String,
     	description: Schema.String,
     }).annotations({
     	title: 'Create API Token Payload',
    @@ -290,7 +291,6 @@ export const CreateApiTokenResponse = Schema.Struct({
      */
     export const DeleteApiTokenPayload = Schema.Struct({
     	tokenID: Schema.String,
    -	userID: Schema.String,
     }).annotations({
     	title: 'Delete API Token Payload',
     	description: 'Payload schema for deleting an existing API token in the StudioCMS Dashboard API.',
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.