Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 107 additions & 35 deletions apps/meteor/app/api/server/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { Meteor } from 'meteor/meteor';

import { isTruthy } from '../../../../lib/isTruthy';
import { adminFields } from '../../../../lib/rooms/adminFields';
import { omit } from '../../../../lib/utils/omit';
import * as dataExport from '../../../../server/lib/dataExport';
import { eraseRoom } from '../../../../server/lib/eraseRoom';
Expand Down Expand Up @@ -1032,49 +1033,120 @@ const isRoomGetRolesPropsSchema = {
additionalProperties: false,
required: ['rid'],
};
export const roomEndpoints = API.v1.get(
'rooms.roles',
{
authRequired: true,
query: ajv.compile<{
rid: string;
}>(isRoomGetRolesPropsSchema),
response: {
200: ajv.compile<{
roles: RoomRoles[];
}>({
type: 'object',
properties: {
roles: {
type: 'array',
items: {
type: 'object',
properties: {
rid: { type: 'string' },
u: {
type: 'object',
properties: { _id: { type: 'string' }, username: { type: 'string' } },
required: ['_id', 'username'],
export const roomEndpoints = API.v1
.get(
'rooms.roles',
{
authRequired: true,
query: ajv.compile<{
rid: string;
}>(isRoomGetRolesPropsSchema),
response: {
200: ajv.compile<{
roles: RoomRoles[];
}>({
type: 'object',
properties: {
roles: {
type: 'array',
items: {
type: 'object',
properties: {
rid: { type: 'string' },
u: {
type: 'object',
properties: { _id: { type: 'string' }, username: { type: 'string' } },
required: ['_id', 'username'],
},
roles: { type: 'array', items: { type: 'string' } },
},
roles: { type: 'array', items: { type: 'string' } },
required: ['rid', 'u', 'roles'],
},
required: ['rid', 'u', 'roles'],
},
},
required: ['roles'],
}),
},
},
async function () {
const { rid } = this.queryParams;
const roles = await executeGetRoomRoles(rid, this.userId);

return API.v1.success({
roles,
});
},
)
.get(
'rooms.adminRooms.privateRooms',
{
authRequired: true,
permissionsRequired: ['view-room-administration'],
query: ajv.compile<{
filter?: string;
offset?: number;
count?: number;
sort?: string;
}>({
type: 'object',
properties: {
filter: { type: 'string' },
offset: { type: 'number' },
count: { type: 'number' },
sort: { type: 'string' },
},
required: ['roles'],
additionalProperties: true,
}),
response: {
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateUnauthorizedErrorResponse,
200: ajv.compile<{
rooms: IRoom[];
count: number;
offset: number;
total: number;
}>({
type: 'object',
properties: {
rooms: {
type: 'array',
items: { type: 'object' },
},
count: { type: 'number' },
offset: { type: 'number' },
total: { type: 'number' },
success: { type: 'boolean', enum: [true] },
},
required: ['rooms', 'count', 'offset', 'total', 'success'],
additionalProperties: false,
}),
},
},
},
async function () {
const { rid } = this.queryParams;
const roles = await executeGetRoomRoles(rid, this.userId);
async function action() {
const { offset, count } = await getPaginationItems(this.queryParams);
const { sort } = await this.parseJsonQuery();
const { filter } = this.queryParams;

return API.v1.success({
roles,
});
},
);
const name = (filter || '').trim();

const { cursor, totalCount } = Rooms.findPrivateRoomsAndTeamsPaginated(name, {
skip: offset,
limit: count,
sort: sort || { default: -1, name: 1 },
projection: adminFields,
});

const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]);

return API.v1.success({
rooms,
count: rooms.length,
offset,
total,
});
},
);

const roomInviteEndpoints = API.v1.post(
'rooms.invite',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const mockRoom2 = createFakeRoom({ t: 'p', name: 'Room 2', fname: 'Room 2' });
const mockRoom3 = createFakeRoom({ t: 'p', name: 'Room 3', fname: 'Room 3', abacAttributes: [] });

const appRoot = mockAppRoot()
.withEndpoint('GET', '/v1/rooms.adminRooms', () => ({
.withEndpoint('GET', '/v1/rooms.adminRooms.privateRooms', () => ({
rooms: [mockRoom1 as any, mockRoom2 as any, mockRoom3 as any],
count: 3,
offset: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ const generateQuery = (
term = '',
): {
filter: string;
types: string[];
} => ({ filter: term, types: ['p'] });
} => ({ filter: term });

type RoomFormAutocompleteProps = Omit<ComponentProps<typeof AutoComplete>, 'filter' | 'onChange'> & {
onSelectedRoom: (value: string, label: string) => void;
Expand All @@ -21,7 +20,7 @@ type RoomFormAutocompleteProps = Omit<ComponentProps<typeof AutoComplete>, 'filt
const RoomFormAutocomplete = ({ value, onSelectedRoom, ...props }: RoomFormAutocompleteProps) => {
const [filter, setFilter] = useState('');
const filterDebounced = useDebouncedValue(filter, 300);
const roomsAutoCompleteEndpoint = useEndpoint('GET', '/v1/rooms.adminRooms');
const roomsAutoCompleteEndpoint = useEndpoint('GET', '/v1/rooms.adminRooms.privateRooms');

const result = useQuery({
queryKey: ABACQueryKeys.rooms.autocomplete(generateQuery(filterDebounced)),
Expand Down
104 changes: 104 additions & 0 deletions apps/meteor/tests/end-to-end/api/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2516,6 +2516,110 @@ describe('[Rooms]', () => {
});
});

describe('/rooms.adminRooms.privateRooms', () => {
let publicChannel: IRoom;
let privateGroup: IRoom;
let publicTeam: ITeam;
let privateTeam: ITeam;

before(async () => {
await updatePermission('view-room-administration', ['admin']);

publicChannel = (await createRoom({ type: 'c', name: `public-channel-${Date.now()}` })).body.channel;
privateGroup = (await createRoom({ type: 'p', name: `private-group-${Date.now()}` })).body.group;

publicTeam = await createTeam(credentials, `public-team-${Date.now()}`, TEAM_TYPE.PUBLIC);
privateTeam = await createTeam(credentials, `private-team-${Date.now()}`, TEAM_TYPE.PRIVATE);
});

after(async () => {
await Promise.all([
deleteRoom({ type: 'c', roomId: publicChannel._id }),
deleteRoom({ type: 'p', roomId: privateGroup._id }),
deleteTeam(credentials, publicTeam.name),
deleteTeam(credentials, privateTeam.name),
]);
});

it('should return only the private room when filtering by its name', async () => {
const res = await request
.get(api('rooms.adminRooms.privateRooms'))
.set(credentials)
.query({
filter: privateGroup.name,
})
.expect(200);

expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('rooms').and.to.be.an('array');

const rooms = res.body.rooms as IRoom[];
expect(rooms).to.have.lengthOf(1);
expect(rooms[0].name).to.equal(privateGroup.name);
expect(rooms[0].t).to.equal('p');
});

it('should return only the private team main when filtering by its name', async () => {
const res = await request
.get(api('rooms.adminRooms.privateRooms'))
.set(credentials)
.query({
filter: privateTeam.name,
})
.expect(200);

expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('rooms').and.to.be.an('array');

const rooms = res.body.rooms as IRoom[];
expect(rooms).to.have.lengthOf(1);
expect(rooms[0].name).to.equal(privateTeam.name);
expect(rooms[0].t).to.equal('p');
});

it('should not return public rooms or public team mains even when filtering by their names', async () => {
const resPublicChannel = await request
.get(api('rooms.adminRooms.privateRooms'))
.set(credentials)
.query({
filter: publicChannel.name,
})
.expect(200);

expect(resPublicChannel.body).to.have.property('success', true);
expect(resPublicChannel.body).to.have.property('rooms').and.to.be.an('array');
expect(resPublicChannel.body.rooms).to.have.lengthOf(0);

const resPublicTeam = await request
.get(api('rooms.adminRooms.privateRooms'))
.set(credentials)
.query({
filter: publicTeam.name,
})
.expect(200);

expect(resPublicTeam.body).to.have.property('success', true);
expect(resPublicTeam.body).to.have.property('rooms').and.to.be.an('array');
expect(resPublicTeam.body.rooms).to.have.lengthOf(0);
});

describe('permissions', () => {
before(async () => {
await updatePermission('view-room-administration', []);
});

after(async () => {
await updatePermission('view-room-administration', ['admin']);
});

it('should return an error for users without view-room-administration permission', async () => {
const res = await request.get(api('rooms.adminRooms.privateRooms')).set(credentials).expect(403);

expect(res.body).to.have.property('success', false);
});
});
});

describe('update group dms name', () => {
let testUser: TestUser<IUser>;
let roomId: IRoom['_id'];
Expand Down
2 changes: 2 additions & 0 deletions packages/model-typings/src/models/IRoomsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export interface IRoomsModel extends IBaseModel<IRoom> {
options?: FindOptions<IRoom>,
): FindPaginated<FindCursor<IRoom>>;

findPrivateRoomsAndTeamsPaginated(name: NonNullable<IRoom['name']>, options?: FindOptions<IRoom>): FindPaginated<FindCursor<IRoom>>;

findByTeamId(teamId: ITeam['_id'], options?: FindOptions<IRoom>): FindCursor<IRoom>;

countByTeamId(teamId: ITeam['_id']): Promise<number>;
Expand Down
20 changes: 20 additions & 0 deletions packages/models/src/models/Rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,26 @@ export class RoomsRaw extends BaseRaw<IRoom> implements IRoomsModel {
return this.findPaginated(query, options);
}

findPrivateRoomsAndTeamsPaginated(name: NonNullable<IRoom['name']>, options: FindOptions<IRoom> = {}): FindPaginated<FindCursor<IRoom>> {
const nameRegex = new RegExp(escapeRegExp(name).trim(), 'i');

const nameCondition: Filter<IRoom> = {
$or: [{ name: nameRegex, federated: { $ne: true } }, { fname: nameRegex }],
};

const query: Filter<IRoom> = {
$and: [
name ? nameCondition : {},
{
t: 'p',
},
],
prid: { $exists: false },
};

return this.findPaginated(query, options);
}

findByTeamId(teamId: ITeam['_id'], options: FindOptions<IRoom> = {}): FindCursor<IRoom> {
const query: Filter<IRoom> = {
teamId,
Expand Down
Loading