From 7f1d45bb5d4a603aca5d767d8f48607d979573d1 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Wed, 26 Jun 2024 20:00:48 +0200 Subject: [PATCH] chore: minimum required roles guard api-v2 (#15576) * chore: minimum required roles guard api-v2 * fixup! chore: minimum required roles guard api-v2 * fixup! fixup! chore: minimum required roles guard api-v2 * fixup! Merge branch 'chore-roles-guard-api-v2' of github.com:calcom/cal.com into chore-roles-guard-api-v2 * fixup! Merge branch 'chore-roles-guard-api-v2' of github.com:calcom/cal.com into chore-roles-guard-api-v2 --- apps/api/v2/src/lib/roles/constants.ts | 13 ++ .../roles/membership-roles.decorator.ts | 4 + .../auth/decorators/roles/roles.decorator.ts | 6 +- .../organization-roles.guard.ts | 4 +- .../modules/auth/guards/roles/roles.guard.ts | 148 ++++++++++++++++++ .../billing/controllers/billing.controller.ts | 6 +- .../memberships/memberships.repository.ts | 17 ++ .../oauth-clients/oauth-clients.controller.ts | 14 +- ...organizations-teams.controller.e2e-spec.ts | 9 ++ .../organizations-teams.controller.ts | 5 +- .../organizations/organizations.module.ts | 3 +- apps/api/v2/swagger/documentation.json | 88 +++++++++-- 12 files changed, 291 insertions(+), 26 deletions(-) create mode 100644 apps/api/v2/src/lib/roles/constants.ts create mode 100644 apps/api/v2/src/modules/auth/decorators/roles/membership-roles.decorator.ts create mode 100644 apps/api/v2/src/modules/auth/guards/roles/roles.guard.ts diff --git a/apps/api/v2/src/lib/roles/constants.ts b/apps/api/v2/src/lib/roles/constants.ts new file mode 100644 index 0000000000..03a9db1840 --- /dev/null +++ b/apps/api/v2/src/lib/roles/constants.ts @@ -0,0 +1,13 @@ +import { MembershipRole } from "@prisma/client"; + +export const SYSTEM_ADMIN_ROLE = "SYSADMIN"; +export const ORG_ROLES = [ + `ORG_${MembershipRole.OWNER}`, + `ORG_${MembershipRole.ADMIN}`, + `ORG_${MembershipRole.MEMBER}`, +] as const; +export const TEAM_ROLES = [ + `TEAM_${MembershipRole.OWNER}`, + `TEAM_${MembershipRole.ADMIN}`, + `TEAM_${MembershipRole.MEMBER}`, +] as const; diff --git a/apps/api/v2/src/modules/auth/decorators/roles/membership-roles.decorator.ts b/apps/api/v2/src/modules/auth/decorators/roles/membership-roles.decorator.ts new file mode 100644 index 0000000000..1165605374 --- /dev/null +++ b/apps/api/v2/src/modules/auth/decorators/roles/membership-roles.decorator.ts @@ -0,0 +1,4 @@ +import { Reflector } from "@nestjs/core"; +import { MembershipRole } from "@prisma/client"; + +export const MembershipRoles = Reflector.createDecorator(); diff --git a/apps/api/v2/src/modules/auth/decorators/roles/roles.decorator.ts b/apps/api/v2/src/modules/auth/decorators/roles/roles.decorator.ts index d1a4511770..1a0256b080 100644 --- a/apps/api/v2/src/modules/auth/decorators/roles/roles.decorator.ts +++ b/apps/api/v2/src/modules/auth/decorators/roles/roles.decorator.ts @@ -1,4 +1,6 @@ +import { SYSTEM_ADMIN_ROLE, ORG_ROLES, TEAM_ROLES } from "@/lib/roles/constants"; import { Reflector } from "@nestjs/core"; -import { MembershipRole } from "@prisma/client"; -export const Roles = Reflector.createDecorator(); +export const Roles = Reflector.createDecorator< + (typeof ORG_ROLES)[number] | (typeof TEAM_ROLES)[number] | typeof SYSTEM_ADMIN_ROLE +>(); diff --git a/apps/api/v2/src/modules/auth/guards/organization-roles/organization-roles.guard.ts b/apps/api/v2/src/modules/auth/guards/organization-roles/organization-roles.guard.ts index c55702be88..bb6eeffe58 100644 --- a/apps/api/v2/src/modules/auth/guards/organization-roles/organization-roles.guard.ts +++ b/apps/api/v2/src/modules/auth/guards/organization-roles/organization-roles.guard.ts @@ -1,4 +1,4 @@ -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; +import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { OrganizationsService } from "@/modules/organizations/services/organizations.service"; import { UserWithProfile } from "@/modules/users/users.repository"; @@ -27,7 +27,7 @@ export class OrganizationRolesGuard implements CanActivate { await this.isPlatform(organizationId); const membership = await this.membershipRepository.findOrgUserMembership(organizationId, user.id); - const allowedRoles = this.reflector.get(Roles, context.getHandler()); + const allowedRoles = this.reflector.get(MembershipRoles, context.getHandler()); this.isMembershipAccepted(membership.accepted); this.isRoleAllowed(membership.role, allowedRoles); diff --git a/apps/api/v2/src/modules/auth/guards/roles/roles.guard.ts b/apps/api/v2/src/modules/auth/guards/roles/roles.guard.ts new file mode 100644 index 0000000000..7a950b21ca --- /dev/null +++ b/apps/api/v2/src/modules/auth/guards/roles/roles.guard.ts @@ -0,0 +1,148 @@ +import { ORG_ROLES, TEAM_ROLES, SYSTEM_ADMIN_ROLE } from "@/lib/roles/constants"; +import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator"; +import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; +import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; +import { Injectable, CanActivate, ExecutionContext, ForbiddenException, Logger } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { Request } from "express"; + +import { Team } from "@calcom/prisma/client"; + +@Injectable() +export class RolesGuard implements CanActivate { + private readonly logger = new Logger("RolesGuard Logger"); + constructor(private reflector: Reflector, private membershipRepository: MembershipsRepository) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const teamId = request.params.teamId as string; + const orgId = request.params.orgId as string; + const user = request.user as GetUserReturnType; + const allowedRole = this.reflector.get(Roles, context.getHandler()); + + // User is not authenticated + if (!user) { + this.logger.log("User is not authenticated, denying access."); + return false; + } + + // System admin can access everything + if (user.isSystemAdmin) { + this.logger.log(`User (${user.id}) is system admin, allowing access.`); + return true; + } + + // if the required role is SYSTEM_ADMIN_ROLE but user is not system admin, return false + if (allowedRole === SYSTEM_ADMIN_ROLE && !user.isSystemAdmin) { + this.logger.log(`User (${user.id}) is not system admin, denying access.`); + return false; + } + + // Checking the role of the user within the organization + if (Boolean(orgId) && !Boolean(teamId)) { + const membership = await this.membershipRepository.findMembershipByOrgId(Number(orgId), user.id); + if (!membership) { + this.logger.log(`User (${user.id}) is not a member of the organization (${orgId}), denying access.`); + throw new ForbiddenException(`User is not a member of the organization.`); + } + + if (ORG_ROLES.includes(allowedRole as unknown as (typeof ORG_ROLES)[number])) { + return hasMinimumRole({ + checkRole: `ORG_${membership.role}`, + minimumRole: allowedRole, + roles: ORG_ROLES, + }); + } + } + + // Checking the role of the user within the team + if (Boolean(teamId) && !Boolean(orgId)) { + const membership = await this.membershipRepository.findMembershipByTeamId(Number(teamId), user.id); + if (!membership) { + this.logger.log(`User (${user.id}) is not a member of the team (${teamId}), denying access.`); + throw new ForbiddenException(`User is not a member of the team.`); + } + if (TEAM_ROLES.includes(allowedRole as unknown as (typeof TEAM_ROLES)[number])) { + return hasMinimumRole({ + checkRole: `TEAM_${membership.role}`, + minimumRole: allowedRole, + roles: TEAM_ROLES, + }); + } + } + + // Checking the role for team and org, org is above team in term of permissions + if (Boolean(teamId) && Boolean(orgId)) { + const teamMembership = await this.membershipRepository.findMembershipByTeamId(Number(teamId), user.id); + const orgMembership = await this.membershipRepository.findMembershipByOrgId(Number(orgId), user.id); + + if (!orgMembership) { + this.logger.log(`User (${user.id}) is not part of the organization (${orgId}), denying access.`); + throw new ForbiddenException(`User is not part of the organization.`); + } + + // if the role checked is a TEAM role + if (TEAM_ROLES.includes(allowedRole as unknown as (typeof TEAM_ROLES)[number])) { + // if the user is admin or owner of org, allow request because org > team + if (`ORG_${orgMembership.role}` === "ORG_ADMIN" || `ORG_${orgMembership.role}` === "ORG_OWNER") { + return true; + } + + if (!teamMembership) { + this.logger.log( + `User (${user.id}) is not part of the team (${teamId}) and/or, is not an admin nor an owner of the organization (${orgId}).` + ); + throw new ForbiddenException( + "User is not part of the team and/or, is not an admin nor an owner of the organization." + ); + } + + // if user is not admin nor an owner of org, and is part of the team, then check user team membership role + return hasMinimumRole({ + checkRole: `TEAM_${teamMembership.role}`, + minimumRole: allowedRole, + roles: TEAM_ROLES, + }); + } + + // if allowed role is a ORG ROLE, check org membersip role + if (ORG_ROLES.includes(allowedRole as unknown as (typeof ORG_ROLES)[number])) { + return hasMinimumRole({ + checkRole: `ORG_${orgMembership.role}`, + minimumRole: allowedRole, + roles: ORG_ROLES, + }); + } + } + + return false; + } +} + +type Roles = (typeof ORG_ROLES)[number] | (typeof TEAM_ROLES)[number]; + +type HasMinimumTeamRoleProp = { + checkRole: (typeof TEAM_ROLES)[number]; + minimumRole: string; + roles: typeof TEAM_ROLES; +}; + +type HasMinimumOrgRoleProp = { + checkRole: (typeof ORG_ROLES)[number]; + minimumRole: string; + roles: typeof ORG_ROLES; +}; + +type HasMinimumRoleProp = HasMinimumTeamRoleProp | HasMinimumOrgRoleProp; + +export function hasMinimumRole(props: HasMinimumRoleProp): boolean { + const checkedRoleIndex = props.roles.indexOf(props.checkRole as never); + const requiredRoleIndex = props.roles.indexOf(props.minimumRole as never); + + // minimum role given does not exist + if (checkedRoleIndex === -1 || requiredRoleIndex === -1) { + throw new Error("Invalid role"); + } + + return checkedRoleIndex <= requiredRoleIndex; +} diff --git a/apps/api/v2/src/modules/billing/controllers/billing.controller.ts b/apps/api/v2/src/modules/billing/controllers/billing.controller.ts index 828ada839f..3317d0d1e3 100644 --- a/apps/api/v2/src/modules/billing/controllers/billing.controller.ts +++ b/apps/api/v2/src/modules/billing/controllers/billing.controller.ts @@ -1,6 +1,6 @@ import { AppConfig } from "@/config/type"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; +import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator"; import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard"; import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard"; import { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subscribe-to-plan.input"; @@ -47,7 +47,7 @@ export class BillingController { @Get("/:teamId/check") @UseGuards(NextAuthGuard, OrganizationRolesGuard) - @Roles(["OWNER", "ADMIN", "MEMBER"]) + @MembershipRoles(["OWNER", "ADMIN", "MEMBER"]) async checkTeamBilling( @Param("teamId") teamId: number ): Promise> { @@ -64,7 +64,7 @@ export class BillingController { @Post("/:teamId/subscribe") @UseGuards(NextAuthGuard, OrganizationRolesGuard) - @Roles(["OWNER", "ADMIN"]) + @MembershipRoles(["OWNER", "ADMIN"]) async subscribeTeamToStripe( @Param("teamId") teamId: number, @Body() input: SubscribeToPlanInput diff --git a/apps/api/v2/src/modules/memberships/memberships.repository.ts b/apps/api/v2/src/modules/memberships/memberships.repository.ts index 2a8e7d1a47..37e950b006 100644 --- a/apps/api/v2/src/modules/memberships/memberships.repository.ts +++ b/apps/api/v2/src/modules/memberships/memberships.repository.ts @@ -18,6 +18,23 @@ export class MembershipsRepository { return membership; } + async findMembershipByTeamId(teamId: number, userId: number) { + const membership = await this.dbRead.prisma.membership.findUnique({ + where: { + userId_teamId: { + userId: userId, + teamId: teamId, + }, + }, + }); + + return membership; + } + + async findMembershipByOrgId(orgId: number, userId: number) { + return this.findMembershipByTeamId(orgId, userId); + } + async isUserOrganizationAdmin(userId: number, organizationId: number) { const adminMembership = await this.dbRead.prisma.membership.findFirst({ where: { diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts index dba95be466..f80dccf14a 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts @@ -1,7 +1,7 @@ import { getEnv } from "@/env"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; -import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; +import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator"; import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard"; import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard"; import { GetManagedUsersOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/get-managed-users.output"; @@ -64,7 +64,7 @@ export class OAuthClientsController { @Post("/") @HttpCode(HttpStatus.CREATED) - @Roles([MembershipRole.ADMIN, MembershipRole.OWNER]) + @MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER]) @DocsOperation({ description: AUTH_DOCUMENTATION }) @DocsCreatedResponse({ description: "Create an OAuth client", @@ -96,7 +96,7 @@ export class OAuthClientsController { @Get("/") @HttpCode(HttpStatus.OK) - @Roles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER]) + @MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER]) @DocsOperation({ description: AUTH_DOCUMENTATION }) async getOAuthClients(@GetUser() user: UserWithProfile): Promise { const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number; @@ -107,7 +107,7 @@ export class OAuthClientsController { @Get("/:clientId") @HttpCode(HttpStatus.OK) - @Roles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER]) + @MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER]) @DocsOperation({ description: AUTH_DOCUMENTATION }) async getOAuthClientById(@Param("clientId") clientId: string): Promise { const client = await this.oauthClientRepository.getOAuthClient(clientId); @@ -119,7 +119,7 @@ export class OAuthClientsController { @Get("/:clientId/managed-users") @HttpCode(HttpStatus.OK) - @Roles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER]) + @MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER]) @DocsOperation({ description: AUTH_DOCUMENTATION }) async getOAuthClientManagedUsersById( @Param("clientId") clientId: string, @@ -137,7 +137,7 @@ export class OAuthClientsController { @Patch("/:clientId") @HttpCode(HttpStatus.OK) - @Roles([MembershipRole.ADMIN, MembershipRole.OWNER]) + @MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER]) @DocsOperation({ description: AUTH_DOCUMENTATION }) async updateOAuthClient( @Param("clientId") clientId: string, @@ -150,7 +150,7 @@ export class OAuthClientsController { @Delete("/:clientId") @HttpCode(HttpStatus.OK) - @Roles([MembershipRole.ADMIN, MembershipRole.OWNER]) + @MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER]) @DocsOperation({ description: AUTH_DOCUMENTATION }) async deleteOAuthClient(@Param("clientId") clientId: string): Promise { this.logger.log(`Deleting OAuth Client with ID: ${clientId}`); diff --git a/apps/api/v2/src/modules/organizations/controllers/organizations-teams.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/controllers/organizations-teams.controller.e2e-spec.ts index 9a1535055a..eb332a420b 100644 --- a/apps/api/v2/src/modules/organizations/controllers/organizations-teams.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/controllers/organizations-teams.controller.e2e-spec.ts @@ -8,6 +8,7 @@ import { NestExpressApplication } from "@nestjs/platform-express"; import { Test } from "@nestjs/testing"; import { User } from "@prisma/client"; import * as request from "supertest"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { withApiAuth } from "test/utils/withApiAuth"; @@ -23,6 +24,7 @@ describe("Organizations Team Endpoints", () => { let userRepositoryFixture: UserRepositoryFixture; let organizationsRepositoryFixture: TeamRepositoryFixture; let teamsRepositoryFixture: TeamRepositoryFixture; + let membershipsRepositoryFixture: MembershipRepositoryFixture; let org: Team; let team: Team; @@ -41,6 +43,7 @@ describe("Organizations Team Endpoints", () => { userRepositoryFixture = new UserRepositoryFixture(moduleRef); organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef); teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); user = await userRepositoryFixture.create({ email: userEmail, @@ -52,6 +55,12 @@ describe("Organizations Team Endpoints", () => { isOrganization: true, }); + await membershipsRepositoryFixture.create({ + role: "ADMIN", + user: { connect: { id: user.id } }, + team: { connect: { id: org.id } }, + }); + team = await teamsRepositoryFixture.create({ name: "Test org team", isOrganization: false, diff --git a/apps/api/v2/src/modules/organizations/controllers/organizations-teams.controller.ts b/apps/api/v2/src/modules/organizations/controllers/organizations-teams.controller.ts index 9742b4c176..c6f5d6a2f1 100644 --- a/apps/api/v2/src/modules/organizations/controllers/organizations-teams.controller.ts +++ b/apps/api/v2/src/modules/organizations/controllers/organizations-teams.controller.ts @@ -1,8 +1,10 @@ import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { GetOrg } from "@/modules/auth/decorators/get-org/get-org.decorator"; import { GetTeam } from "@/modules/auth/decorators/get-team/get-team.decorator"; +import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard"; +import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; import { Controller, UseGuards, Get, Param, ParseIntPipe } from "@nestjs/common"; import { ApiTags as DocsTags } from "@nestjs/swagger"; @@ -31,7 +33,8 @@ export class OrganizationsTeamsController { }; } - @UseGuards(IsTeamInOrg) + @UseGuards(IsTeamInOrg, RolesGuard) + @Roles("ORG_ADMIN") @Get("/:teamId") async getTeam( @Param("orgId", ParseIntPipe) orgId: number, diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index 43f784673d..ba2f9a89b3 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -1,3 +1,4 @@ +import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { OrganizationsTeamsController } from "@/modules/organizations/controllers/organizations-teams.controller"; import { OrganizationsRepository } from "@/modules/organizations/organizations.repository"; import { OrganizationsService } from "@/modules/organizations/services/organizations.service"; @@ -7,7 +8,7 @@ import { Module } from "@nestjs/common"; @Module({ imports: [PrismaModule, StripeModule], - providers: [OrganizationsRepository, OrganizationsService], + providers: [OrganizationsRepository, OrganizationsService, MembershipsRepository], exports: [OrganizationsService, OrganizationsRepository], controllers: [OrganizationsTeamsController], }) diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 2bfed78bce..66e81576f6 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -806,6 +806,74 @@ ] } }, + "/v2/organizations/{orgId}/teams": { + "get": { + "operationId": "OrganizationsTeamsController_getAllTeams", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}": { + "get": { + "operationId": "OrganizationsTeamsController_getTeam", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, "/v2/schedules": { "post": { "operationId": "SchedulesController_2024_06_11_createSchedule", @@ -2185,11 +2253,11 @@ "properties": { "status": { "type": "string", - "example": "success", "enum": [ "success", "error" - ] + ], + "example": "success" }, "data": { "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" @@ -2205,11 +2273,11 @@ "properties": { "status": { "type": "string", - "example": "success", "enum": [ "success", "error" - ] + ], + "example": "success" }, "data": { "nullable": true, @@ -2230,11 +2298,11 @@ "properties": { "status": { "type": "string", - "example": "success", "enum": [ "success", "error" - ] + ], + "example": "success" }, "data": { "type": "array", @@ -2257,11 +2325,11 @@ "properties": { "status": { "type": "string", - "example": "success", "enum": [ "success", "error" - ] + ], + "example": "success" }, "data": { "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" @@ -2303,11 +2371,11 @@ "properties": { "status": { "type": "string", - "example": "success", "enum": [ "success", "error" - ] + ], + "example": "success" }, "data": { "$ref": "#/components/schemas/DeleteData_2024_06_14"