backend: enhance zod schemas for room members and users, adding new validation and permissions structures

This commit is contained in:
juancarmore 2025-12-05 15:23:28 +01:00
parent e3de4256a8
commit e0736677ca
4 changed files with 229 additions and 80 deletions

View File

@ -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<AuthMode> = z.nativeEnum(AuthMode);
const AuthTypeSchema: z.ZodType<AuthType> = z.nativeEnum(AuthType);
const SingleUserAuthSchema: z.ZodType<SingleUserAuth> = z.object({
type: AuthTypeSchema
});
const ValidAuthMethodSchema: z.ZodType<ValidAuthMethod> = SingleUserAuthSchema;
const AuthenticationConfigSchema: z.ZodType<AuthenticationConfig> = z.object({
authMethod: ValidAuthMethodSchema,
authModeToAccessRoom: AuthModeSchema
allowUserCreation: z.boolean()
});
export const SecurityConfigSchema: z.ZodType<SecurityConfig> = z.object({

View File

@ -0,0 +1,132 @@
import {
MeetRoomMemberFilters,
MeetRoomMemberOptions,
MeetRoomMemberPermissions,
MeetRoomMemberRole,
MeetRoomMemberTokenMetadata,
MeetRoomMemberTokenOptions
} from '@openvidu-meet/typings';
import { z } from 'zod';
const RoomMemberRoleSchema: z.ZodType<MeetRoomMemberRole> = z.nativeEnum(MeetRoomMemberRole);
export const MeetPermissionsSchema: z.ZodType<MeetRoomMemberPermissions> = 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<Partial<MeetRoomMemberPermissions>> = 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<MeetRoomMemberOptions> = 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<MeetRoomMemberFilters> = 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<MeetRoomMemberTokenOptions> = 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<MeetRoomMemberTokenMetadata> = z.object({
livekitUrl: z.string().url('LiveKit URL must be a valid URL'),
roomId: z.string(),
baseRole: RoomMemberRoleSchema,
customPermissions: PartialMeetPermissionsSchema.optional(),
effectivePermissions: MeetPermissionsSchema
});

View File

@ -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<MeetRecordingAccess> = z.nativeEnum(MeetRecordingAccess);
const RecordingConfigSchema: z.ZodType<MeetRecordingConfig> = 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<MeetRecordingConfig> = z.object({
enabled: z.boolean()
});
const ChatConfigSchema: z.ZodType<MeetChatConfig> = z.object({
enabled: z.boolean()
@ -103,10 +87,7 @@ const RoomConfigSchema: z.ZodType<Partial<MeetRoomConfig>> = 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<MeetRoomAutoDeletionPolicy> = z.ob
withRecordings: RoomDeletionPolicyWithRecordingsSchema
});
const RoomRolesConfigSchema: z.ZodType<MeetRoomRolesConfig> = z.object({
moderator: z
.object({
permissions: PartialMeetPermissionsSchema
})
.optional(),
speaker: z
.object({
permissions: PartialMeetPermissionsSchema
})
.optional()
});
const RoomAnonymousConfigSchema: z.ZodType<MeetRoomAnonymousConfig> = z.object({
moderator: z
.object({
enabled: z.boolean()
})
.optional(),
speaker: z
.object({
enabled: z.boolean()
})
.optional()
});
export const RoomOptionsSchema: z.ZodType<MeetRoomOptions> = z.object({
roomName: z
.string()
@ -164,10 +171,15 @@ export const RoomOptionsSchema: z.ZodType<MeetRoomOptions> = 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<MeetRoomMemberTokenOptions> = 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<MeetPermissions> = z.object({
canRecord: z.boolean(),
canRetrieveRecordings: z.boolean(),
canDeleteRecordings: z.boolean(),
canChat: z.boolean(),
canChangeVirtualBackground: z.boolean()
});
export const RoomMemberTokenMetadataSchema: z.ZodType<MeetRoomMemberTokenMetadata> = z.object({
livekitUrl: z.string().url('LiveKit URL must be a valid URL'),
role: z.nativeEnum(MeetRoomMemberRole),
permissions: MeetPermissionsSchema
});

View File

@ -1,5 +1,53 @@
import { MeetUserFilters, MeetUserOptions, MeetUserRole } from '@openvidu-meet/typings';
import { z } from 'zod';
export const CreateUserReqSchema: z.ZodType<MeetUserOptions> = 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<MeetUserFilters> = 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')