diff --git a/meet-ce/backend/src/models/zod-schemas/global-config.schema.ts b/meet-ce/backend/src/models/zod-schemas/global-config.schema.ts index 3b53b5a6..a9b441ca 100644 --- a/meet-ce/backend/src/models/zod-schemas/global-config.schema.ts +++ b/meet-ce/backend/src/models/zod-schemas/global-config.schema.ts @@ -1,12 +1,4 @@ -import { - AuthenticationConfig, - AuthMode, - AuthType, - SecurityConfig, - SingleUserAuth, - ValidAuthMethod, - WebhookConfig -} from '@openvidu-meet/typings'; +import { AuthenticationConfig, SecurityConfig, WebhookConfig } from '@openvidu-meet/typings'; import { z } from 'zod'; import { AppearanceConfigSchema } from './room.schema.js'; @@ -37,19 +29,8 @@ export const TestWebhookReqSchema = z.object({ .regex(/^https?:\/\//, { message: 'URL must start with http:// or https://' }) }); -const AuthModeSchema: z.ZodType = z.nativeEnum(AuthMode); - -const AuthTypeSchema: z.ZodType = z.nativeEnum(AuthType); - -const SingleUserAuthSchema: z.ZodType = z.object({ - type: AuthTypeSchema -}); - -const ValidAuthMethodSchema: z.ZodType = SingleUserAuthSchema; - const AuthenticationConfigSchema: z.ZodType = z.object({ - authMethod: ValidAuthMethodSchema, - authModeToAccessRoom: AuthModeSchema + allowUserCreation: z.boolean() }); export const SecurityConfigSchema: z.ZodType = z.object({ diff --git a/meet-ce/backend/src/models/zod-schemas/room-member.schema.ts b/meet-ce/backend/src/models/zod-schemas/room-member.schema.ts new file mode 100644 index 00000000..669875ef --- /dev/null +++ b/meet-ce/backend/src/models/zod-schemas/room-member.schema.ts @@ -0,0 +1,132 @@ +import { + MeetRoomMemberFilters, + MeetRoomMemberOptions, + MeetRoomMemberPermissions, + MeetRoomMemberRole, + MeetRoomMemberTokenMetadata, + MeetRoomMemberTokenOptions +} from '@openvidu-meet/typings'; +import { z } from 'zod'; + +const RoomMemberRoleSchema: z.ZodType = z.nativeEnum(MeetRoomMemberRole); + +export const MeetPermissionsSchema: z.ZodType = z.object({ + canRecord: z.boolean(), + canRetrieveRecordings: z.boolean(), + canDeleteRecordings: z.boolean(), + canJoinMeeting: z.boolean(), + canShareAccessLinks: z.boolean(), + canMakeModerator: z.boolean(), + canKickParticipants: z.boolean(), + canEndMeeting: z.boolean(), + canPublishVideo: z.boolean(), + canPublishAudio: z.boolean(), + canShareScreen: z.boolean(), + canReadChat: z.boolean(), + canWriteChat: z.boolean(), + canChangeVirtualBackground: z.boolean() +}); + +export const PartialMeetPermissionsSchema: z.ZodType> = z.object({ + canRecord: z.boolean().optional(), + canRetrieveRecordings: z.boolean().optional(), + canDeleteRecordings: z.boolean().optional(), + canJoinMeeting: z.boolean().optional(), + canShareAccessLinks: z.boolean().optional(), + canMakeModerator: z.boolean().optional(), + canKickParticipants: z.boolean().optional(), + canEndMeeting: z.boolean().optional(), + canPublishVideo: z.boolean().optional(), + canPublishAudio: z.boolean().optional(), + canShareScreen: z.boolean().optional(), + canReadChat: z.boolean().optional(), + canWriteChat: z.boolean().optional(), + canChangeVirtualBackground: z.boolean().optional() +}); + +export const RoomMemberOptionsSchema: z.ZodType = z + .object({ + userId: z + .string() + .regex(/^[a-z0-9_]+$/, 'userId must contain only lowercase letters, numbers, and underscores') + .optional(), + name: z.string().min(1, 'name cannot be empty').max(50, 'name cannot exceed 50 characters').optional(), + baseRole: RoomMemberRoleSchema, + customPermissions: PartialMeetPermissionsSchema.optional() + }) + .refine( + (data) => { + // Either userId or name must be provided, but not both + return (data.userId && !data.name) || (!data.userId && data.name); + }, + { + message: 'Either userId or name must be provided, but not both', + path: ['userId'] + } + ); + +export const RoomMemberFiltersSchema: z.ZodType = z.object({ + name: z.string().optional(), + fields: z.string().optional(), + maxItems: z.coerce + .number() + .positive('maxItems must be a positive number') + .transform((val) => { + // Convert the value to a number + const intVal = Math.floor(val); + // Ensure it's not greater than 100 + return intVal > 100 ? 100 : intVal; + }) + .default(10), + nextPageToken: z.string().optional() +}); + +export const BulkDeleteRoomMembersReqSchema = z.object({ + memberIds: z.preprocess( + (arg) => { + if (typeof arg === 'string') { + // If the argument is a string, it is expected to be a comma-separated list of member IDs. + return arg + .split(',') + .map((s) => s.trim()) + .filter((s) => s !== ''); + } + + return []; + }, + z.array(z.string()).min(1, { + message: 'At least one memberId is required' + }) + ) +}); + +export const UpdateRoomMemberReqSchema = z.object({ + baseRole: RoomMemberRoleSchema.optional(), + customPermissions: PartialMeetPermissionsSchema.optional() +}); + +export const RoomMemberTokenOptionsSchema: z.ZodType = z + .object({ + secret: z.string().optional(), + grantJoinMeetingPermission: z.boolean().optional().default(false), + participantName: z.string().optional(), + participantIdentity: z.string().optional() + }) + .refine( + (data) => { + // If grantJoinMeetingPermission is true, participantName must be provided + return !data.grantJoinMeetingPermission || data.participantName; + }, + { + message: 'participantName is required when grantJoinMeetingPermission is true', + path: ['participantName'] + } + ); + +export const RoomMemberTokenMetadataSchema: z.ZodType = z.object({ + livekitUrl: z.string().url('LiveKit URL must be a valid URL'), + roomId: z.string(), + baseRole: RoomMemberRoleSchema, + customPermissions: PartialMeetPermissionsSchema.optional(), + effectivePermissions: MeetPermissionsSchema +}); diff --git a/meet-ce/backend/src/models/zod-schemas/room.schema.ts b/meet-ce/backend/src/models/zod-schemas/room.schema.ts index c3c1720a..ec0cafb6 100644 --- a/meet-ce/backend/src/models/zod-schemas/room.schema.ts +++ b/meet-ce/backend/src/models/zod-schemas/room.schema.ts @@ -2,18 +2,15 @@ import { MeetAppearanceConfig, MeetChatConfig, MeetE2EEConfig, - MeetPermissions, - MeetRecordingAccess, MeetRecordingConfig, + MeetRoomAnonymousConfig, MeetRoomAutoDeletionPolicy, MeetRoomConfig, MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, MeetRoomFilters, - MeetRoomMemberRole, - MeetRoomMemberTokenMetadata, - MeetRoomMemberTokenOptions, MeetRoomOptions, + MeetRoomRolesConfig, MeetRoomStatus, MeetRoomTheme, MeetRoomThemeMode, @@ -23,6 +20,7 @@ import ms from 'ms'; import { z } from 'zod'; import { INTERNAL_CONFIG } from '../../config/internal-config.js'; import { MeetRoomHelper } from '../../helpers/room.helper.js'; +import { PartialMeetPermissionsSchema } from './room-member.schema.js'; export const nonEmptySanitizedRoomId = (fieldName: string) => z @@ -34,23 +32,9 @@ export const nonEmptySanitizedRoomId = (fieldName: string) => message: `${fieldName} cannot be empty after sanitization` }); -const RecordingAccessSchema: z.ZodType = z.nativeEnum(MeetRecordingAccess); - -const RecordingConfigSchema: z.ZodType = z - .object({ - enabled: z.boolean(), - allowAccessTo: RecordingAccessSchema.optional() - }) - .refine( - (data) => { - // If recording is enabled, allowAccessTo must be provided - return !data.enabled || data.allowAccessTo !== undefined; - }, - { - message: 'allowAccessTo is required when recording is enabled', - path: ['allowAccessTo'] - } - ); +const RecordingConfigSchema: z.ZodType = z.object({ + enabled: z.boolean() +}); const ChatConfigSchema: z.ZodType = z.object({ enabled: z.boolean() @@ -103,10 +87,7 @@ const RoomConfigSchema: z.ZodType> = z .transform((data) => { // Automatically disable recording when E2EE is enabled if (data.e2ee?.enabled && data.recording?.enabled) { - data.recording = { - ...data.recording, - enabled: false - }; + data.recording.enabled = false; } return data; @@ -125,6 +106,32 @@ const RoomAutoDeletionPolicySchema: z.ZodType = z.ob withRecordings: RoomDeletionPolicyWithRecordingsSchema }); +const RoomRolesConfigSchema: z.ZodType = z.object({ + moderator: z + .object({ + permissions: PartialMeetPermissionsSchema + }) + .optional(), + speaker: z + .object({ + permissions: PartialMeetPermissionsSchema + }) + .optional() +}); + +const RoomAnonymousConfigSchema: z.ZodType = z.object({ + moderator: z + .object({ + enabled: z.boolean() + }) + .optional(), + speaker: z + .object({ + enabled: z.boolean() + }) + .optional() +}); + export const RoomOptionsSchema: z.ZodType = z.object({ roomName: z .string() @@ -164,10 +171,15 @@ export const RoomOptionsSchema: z.ZodType = z.object({ } ), config: RoomConfigSchema.optional().default({ - recording: { enabled: true, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER }, + recording: { enabled: true }, chat: { enabled: true }, virtualBackground: { enabled: true }, e2ee: { enabled: false } + }), + roles: RoomRolesConfigSchema.optional(), + anonymous: RoomAnonymousConfigSchema.optional().default({ + moderator: { enabled: true }, + speaker: { enabled: true } }) // maxParticipants: z // .number() @@ -244,38 +256,14 @@ export const UpdateRoomConfigReqSchema = z.object({ config: RoomConfigSchema }); +export const UpdateRoomRolesReqSchema = z.object({ + roles: RoomRolesConfigSchema +}); + +export const UpdateRoomAnonymousReqSchema = z.object({ + anonymous: RoomAnonymousConfigSchema +}); + export const UpdateRoomStatusReqSchema = z.object({ status: z.enum([MeetRoomStatus.OPEN, MeetRoomStatus.CLOSED]) }); - -export const RoomMemberTokenOptionsSchema: z.ZodType = z - .object({ - secret: z.string().nonempty('Secret is required'), - grantJoinMeetingPermission: z.boolean().optional().default(false), - participantName: z.string().optional(), - participantIdentity: z.string().optional() - }) - .refine( - (data) => { - // If grantJoinMeetingPermission is true, participantName must be provided - return !data.grantJoinMeetingPermission || data.participantName; - }, - { - message: 'participantName is required when grantJoinMeetingPermission is true', - path: ['participantName'] - } - ); - -const MeetPermissionsSchema: z.ZodType = z.object({ - canRecord: z.boolean(), - canRetrieveRecordings: z.boolean(), - canDeleteRecordings: z.boolean(), - canChat: z.boolean(), - canChangeVirtualBackground: z.boolean() -}); - -export const RoomMemberTokenMetadataSchema: z.ZodType = z.object({ - livekitUrl: z.string().url('LiveKit URL must be a valid URL'), - role: z.nativeEnum(MeetRoomMemberRole), - permissions: MeetPermissionsSchema -}); diff --git a/meet-ce/backend/src/models/zod-schemas/user.schema.ts b/meet-ce/backend/src/models/zod-schemas/user.schema.ts index cee12f9d..e9d58c67 100644 --- a/meet-ce/backend/src/models/zod-schemas/user.schema.ts +++ b/meet-ce/backend/src/models/zod-schemas/user.schema.ts @@ -1,5 +1,53 @@ +import { MeetUserFilters, MeetUserOptions, MeetUserRole } from '@openvidu-meet/typings'; import { z } from 'zod'; +export const CreateUserReqSchema: z.ZodType = z.object({ + userId: z + .string() + .min(5, 'userId must be at least 5 characters long') + .max(20, 'userId cannot exceed 20 characters') + .regex(/^[a-z0-9_]+$/, 'userId must contain only lowercase letters, numbers, and underscores'), + name: z.string().min(1, 'name is required and cannot be empty').max(50, 'name cannot exceed 50 characters'), + role: z.nativeEnum(MeetUserRole), + password: z.string().min(5, 'password must be at least 5 characters long') +}); + +export const UserFiltersSchema: z.ZodType = z.object({ + userId: z.string().optional(), + name: z.string().optional(), + fields: z.string().optional(), + maxItems: z.coerce + .number() + .positive('maxItems must be a positive number') + .transform((val) => { + // Convert the value to a number + const intVal = Math.floor(val); + // Ensure it's not greater than 100 + return intVal > 100 ? 100 : intVal; + }) + .default(10), + nextPageToken: z.string().optional() +}); + +export const BulkDeleteUsersReqSchema = z.object({ + userIds: z.preprocess( + (arg) => { + if (typeof arg === 'string') { + // If the argument is a string, it is expected to be a comma-separated list of user IDs. + return arg + .split(',') + .map((s) => s.trim()) + .filter((s) => s !== ''); + } + + return []; + }, + z.array(z.string()).min(1, { + message: 'At least one userId is required' + }) + ) +}); + export const ChangePasswordReqSchema = z.object({ currentPassword: z.string(), newPassword: z.string().min(5, 'New password must be at least 5 characters long')