mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-16 02:36:19 +00:00
space management without roles
This commit is contained in:
@ -70,6 +70,16 @@ export class ControllerRoute {
|
||||
};
|
||||
};
|
||||
|
||||
static USER_SPACE = class {
|
||||
public static readonly ROUTE = '/user/:userUuid/spaces';
|
||||
static ACTIONS = class {
|
||||
public static readonly GET_USER_SPACES_SUMMARY =
|
||||
'Retrieve list of spaces a user belongs to';
|
||||
public static readonly GET_USER_SPACES_DESCRIPTION =
|
||||
'This endpoint retrieves all the spaces that a user is associated with, based on the user ID. It fetches the user spaces by querying the UserSpaceEntity to find the spaces where the user has an association.';
|
||||
};
|
||||
};
|
||||
|
||||
static SPACE = class {
|
||||
public static readonly ROUTE = '/communities/:communityId/spaces';
|
||||
static ACTIONS = class {
|
||||
@ -93,9 +103,30 @@ export class ControllerRoute {
|
||||
public static readonly UPDATE_SPACE_DESCRIPTION =
|
||||
'Updates a space by its UUID and community ID. You can update the name, parent space, and other properties. If a parent space is provided and not already a parent, its `isParent` flag will be set to true.';
|
||||
|
||||
public static readonly GET_HEIRARCHY_SUMMARY = 'Get spaces hierarchy';
|
||||
public static readonly GET_HEIRARCHY_SUMMARY = 'Get space hierarchy';
|
||||
public static readonly GET_HEIRARCHY_DESCRIPTION =
|
||||
'Fetches all spaces within a community and returns them in a hierarchical structure with parent-child relationships.';
|
||||
'This endpoint retrieves the hierarchical structure of spaces under a given space ID. It returns all the child spaces nested within the specified space, organized by their parent-child relationships. ';
|
||||
|
||||
public static readonly CREATE_INVITATION_CODE_SPACE_SUMMARY =
|
||||
'Generate a new invitation code for a specific space';
|
||||
public static readonly CREATE_INVITATION_CODE_SPACE_DESCRIPTION =
|
||||
'This endpoint generates a new 6-character invitation code for a space identified by its UUID and stores it in the space entity';
|
||||
};
|
||||
};
|
||||
|
||||
static SPACE_USER = class {
|
||||
public static readonly ROUTE =
|
||||
'/communities/:communityId/spaces/:spaceId/user';
|
||||
static ACTIONS = class {
|
||||
public static readonly ASSOCIATE_SPACE_USER_SUMMARY =
|
||||
'Associate a user to a space';
|
||||
public static readonly ASSOCIATE_SPACE_USER_DESCRIPTION =
|
||||
'Associates a user with a given space by their respective UUIDs';
|
||||
|
||||
public static readonly DISSOCIATE_SPACE_USER_SUMMARY =
|
||||
'Disassociate a user from a space';
|
||||
public static readonly DISSOCIATE_SPACE_USER_DESCRIPTION =
|
||||
'Disassociates a user from a space by removing the existing association.';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -67,11 +67,9 @@ export class CommunitySpaceService {
|
||||
const rootSpaces: SpaceEntity[] = [];
|
||||
spaces.forEach((space) => {
|
||||
if (space.parent && space.parent.uuid) {
|
||||
// If the space has a parent, add it to the parent's children array
|
||||
const parent = map.get(space.parent.uuid);
|
||||
parent?.children?.push(map.get(space.uuid));
|
||||
} else {
|
||||
// If the space doesn't have a parent, it's a root space
|
||||
rootSpaces.push(map.get(space.uuid));
|
||||
}
|
||||
});
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './space.controller';
|
||||
export * from './space.controller';
|
||||
export * from './space-user.controller';
|
||||
|
49
src/space/controllers/space-user.controller.ts
Normal file
49
src/space/controllers/space-user.controller.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
import { Controller, Delete, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { SpaceUserService } from '../services';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { UserSpaceDto } from '@app/common/modules/user/dtos';
|
||||
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
|
||||
|
||||
@ApiTags('Space Module')
|
||||
@Controller({
|
||||
version: '1',
|
||||
path: ControllerRoute.SPACE_USER.ROUTE,
|
||||
})
|
||||
export class SpaceUserController {
|
||||
constructor(private readonly spaceUserService: SpaceUserService) {}
|
||||
|
||||
@Post('/:userUuid')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
ControllerRoute.SPACE_USER.ACTIONS.ASSOCIATE_SPACE_USER_DESCRIPTION,
|
||||
description:
|
||||
ControllerRoute.SPACE_USER.ACTIONS.ASSOCIATE_SPACE_USER_DESCRIPTION,
|
||||
})
|
||||
async associateUserToSpace(
|
||||
@Param() params: UserSpaceDto,
|
||||
): Promise<BaseResponseDto> {
|
||||
return this.spaceUserService.associateUserToSpace(
|
||||
params.userUuid,
|
||||
params.spaceUuid,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('/:userUuid')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.SPACE_USER.ACTIONS.DISSOCIATE_SPACE_USER_SUMMARY,
|
||||
description:
|
||||
ControllerRoute.SPACE_USER.ACTIONS.DISSOCIATE_SPACE_USER_DESCRIPTION,
|
||||
})
|
||||
async disassociateUserFromSpace(
|
||||
@Param() params: UserSpaceDto,
|
||||
): Promise<BaseResponseDto> {
|
||||
return this.spaceUserService.disassociateUserFromSpace(
|
||||
params.userUuid,
|
||||
params.spaceUuid,
|
||||
);
|
||||
}
|
||||
}
|
@ -94,6 +94,33 @@ export class SpaceController {
|
||||
})
|
||||
@Get('/:spaceUuid')
|
||||
async get(@Param() params: GetSpaceParam): Promise<BaseResponseDto> {
|
||||
return this.spaceService.findOne(params.spaceUuid, params.communityUuid);
|
||||
return this.spaceService.findOne(params.spaceUuid);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.SPACE.ACTIONS.GET_HEIRARCHY_SUMMARY,
|
||||
description: ControllerRoute.SPACE.ACTIONS.GET_HEIRARCHY_DESCRIPTION,
|
||||
})
|
||||
@Get('/:spaceUuid/hierarchy')
|
||||
async getHierarchyUnderSpace(
|
||||
@Param() params: GetSpaceParam,
|
||||
): Promise<BaseResponseDto> {
|
||||
return this.spaceService.getSpacesHierarchyForSpace(params.spaceUuid);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.SPACE.ACTIONS.CREATE_INVITATION_CODE_SPACE_SUMMARY,
|
||||
description:
|
||||
ControllerRoute.SPACE.ACTIONS.CREATE_INVITATION_CODE_SPACE_DESCRIPTION,
|
||||
})
|
||||
@Post(':spaceUuid/invitation-code')
|
||||
async generateSpaceInvitationCode(
|
||||
@Param() params: GetSpaceParam,
|
||||
): Promise<BaseResponseDto> {
|
||||
return this.spaceService.getSpaceInvitationCode(params.spaceUuid);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from './add.space.dto';
|
||||
export * from './community-space.param';
|
||||
export * from './get.space.param';
|
||||
export * from './user-space.param';
|
||||
|
12
src/space/dtos/user-space.param.ts
Normal file
12
src/space/dtos/user-space.param.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsUUID } from 'class-validator';
|
||||
import { GetSpaceParam } from './get.space.param';
|
||||
|
||||
export class UserSpaceParam extends GetSpaceParam {
|
||||
@ApiProperty({
|
||||
description: 'Uuid of the user to be associated/ dissociated',
|
||||
example: 'd290f1ee-6c54-4b01-90e6-d701748f0851',
|
||||
})
|
||||
@IsUUID()
|
||||
userUuid: string;
|
||||
}
|
@ -1 +1,2 @@
|
||||
export * from './space.service';
|
||||
export * from './space-user.service';
|
||||
|
108
src/space/services/space-user.service.ts
Normal file
108
src/space/services/space-user.service.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
||||
import {
|
||||
UserRepository,
|
||||
UserSpaceRepository,
|
||||
} from '@app/common/modules/user/repositories';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class SpaceUserService {
|
||||
constructor(
|
||||
private readonly spaceRepository: SpaceRepository,
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly userSpaceRepository: UserSpaceRepository,
|
||||
) {}
|
||||
async associateUserToSpace(
|
||||
userUuid: string,
|
||||
spaceUuid: string,
|
||||
): Promise<BaseResponseDto> {
|
||||
// Find the user by ID
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { uuid: userUuid },
|
||||
});
|
||||
if (!user) {
|
||||
throw new HttpException(
|
||||
`User with ID ${userUuid} not found`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
// Find the space by ID
|
||||
const space = await this.spaceRepository.findOne({
|
||||
where: { uuid: spaceUuid },
|
||||
});
|
||||
if (!space) {
|
||||
throw new HttpException(
|
||||
`Space with ID ${spaceUuid} not found`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the association already exists
|
||||
const existingAssociation = await this.userSpaceRepository.findOne({
|
||||
where: { user: { uuid: userUuid }, space: { uuid: spaceUuid } },
|
||||
});
|
||||
if (existingAssociation) {
|
||||
throw new HttpException(
|
||||
`User is already associated with the space`,
|
||||
HttpStatus.CONFLICT,
|
||||
);
|
||||
}
|
||||
|
||||
// Create a new UserSpaceEntity entry to associate the user and the space
|
||||
const userSpace = this.userSpaceRepository.create({ user, space });
|
||||
|
||||
return new SuccessResponseDto({
|
||||
data: userSpace,
|
||||
message: `Space ${spaceUuid} has been successfully associated t user ${userUuid}`,
|
||||
});
|
||||
}
|
||||
|
||||
async disassociateUserFromSpace(
|
||||
userUuid: string,
|
||||
spaceUuid: string,
|
||||
): Promise<BaseResponseDto> {
|
||||
// Find the user by ID
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { uuid: userUuid },
|
||||
});
|
||||
if (!user) {
|
||||
throw new HttpException(
|
||||
`User with ID ${userUuid} not found`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
// Find the space by ID
|
||||
const space = await this.spaceRepository.findOne({
|
||||
where: { uuid: spaceUuid },
|
||||
});
|
||||
if (!space) {
|
||||
throw new HttpException(
|
||||
`Space with ID ${spaceUuid} not found`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
// Find the existing association
|
||||
const existingAssociation = await this.userSpaceRepository.findOne({
|
||||
where: { user: { uuid: userUuid }, space: { uuid: spaceUuid } },
|
||||
});
|
||||
|
||||
if (!existingAssociation) {
|
||||
throw new HttpException(
|
||||
`No association found between user ${userUuid} and space ${spaceUuid}`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the association
|
||||
await this.userSpaceRepository.remove(existingAssociation);
|
||||
|
||||
return new SuccessResponseDto({
|
||||
message: `User ${userUuid} has been successfully disassociated from space ${spaceUuid}`,
|
||||
});
|
||||
}
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { AddSpaceDto } from '../dtos';
|
||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
@ -11,6 +16,7 @@ import {
|
||||
} from '@app/common/models/typeOrmCustom.model';
|
||||
import { SpaceDto } from '@app/common/modules/space/dtos';
|
||||
import { PageResponse } from '@app/common/dto/pagination.response.dto';
|
||||
import { generateRandomString } from '@app/common/helper/randomString';
|
||||
|
||||
@Injectable()
|
||||
export class SpaceService {
|
||||
@ -88,9 +94,8 @@ export class SpaceService {
|
||||
try {
|
||||
pageable.modelName = 'space';
|
||||
pageable.where = {
|
||||
community: { uuid: communityId }, // Ensure the relation is used in the where clause
|
||||
community: { uuid: communityId },
|
||||
};
|
||||
console.log('Where clause before building:', pageable.where);
|
||||
|
||||
const customModel = TypeORMCustomModel(this.spaceRepository);
|
||||
|
||||
@ -102,29 +107,11 @@ export class SpaceService {
|
||||
}
|
||||
}
|
||||
|
||||
async findOne(
|
||||
spaceId: string,
|
||||
communityId: string,
|
||||
): Promise<BaseResponseDto> {
|
||||
async findOne(spaceId: string): Promise<BaseResponseDto> {
|
||||
try {
|
||||
const community = await this.communityRepository.findOne({
|
||||
where: { uuid: communityId },
|
||||
});
|
||||
|
||||
// If the community doesn't exist, throw a 404 error
|
||||
if (!community) {
|
||||
throw new HttpException(
|
||||
`Community with ID ${communityId} not found`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const space = await this.spaceRepository.findOne({
|
||||
where: {
|
||||
uuid: spaceId,
|
||||
community: {
|
||||
uuid: communityId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -268,4 +255,101 @@ export class SpaceService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getSpacesHierarchyForSpace(spaceId: string): Promise<BaseResponseDto> {
|
||||
const space = await this.spaceRepository.findOne({
|
||||
where: { uuid: spaceId },
|
||||
});
|
||||
|
||||
// If the space doesn't exist, throw a 404 error
|
||||
if (!space) {
|
||||
throw new HttpException(
|
||||
`Space with ID ${spaceId} not found`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all spaces that are children of the provided space, including the parent-child relations
|
||||
const spaces = await this.spaceRepository.find({
|
||||
where: { parent: { uuid: spaceId } },
|
||||
relations: ['parent', 'children'], // Include parent and children relations
|
||||
});
|
||||
|
||||
// Organize spaces into a hierarchical structure
|
||||
const spaceHierarchy = this.buildSpaceHierarchy(spaces);
|
||||
|
||||
return new SuccessResponseDto({
|
||||
message: `Spaces under space ${spaceId} successfully fetched in hierarchy`,
|
||||
data: spaceHierarchy,
|
||||
statusCode: HttpStatus.OK,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
'An error occurred while fetching the spaces under the space',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getSpaceInvitationCode(spaceUuid: string): Promise<any> {
|
||||
try {
|
||||
const invitationCode = generateRandomString(6);
|
||||
|
||||
const space = await this.spaceRepository.findOne({
|
||||
where: {
|
||||
uuid: spaceUuid,
|
||||
},
|
||||
});
|
||||
|
||||
if (!space) {
|
||||
throw new HttpException(
|
||||
`Space with ID ${spaceUuid} not found`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
space.invitationCode = invitationCode;
|
||||
await this.spaceRepository.save(space);
|
||||
|
||||
return new SuccessResponseDto({
|
||||
message: `Invitation code has been successfuly added to the space`,
|
||||
data: space,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestException) {
|
||||
throw err;
|
||||
} else {
|
||||
throw new HttpException('Space not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private buildSpaceHierarchy(spaces: SpaceEntity[]): SpaceEntity[] {
|
||||
const map = new Map<string, SpaceEntity>();
|
||||
|
||||
// Step 1: Create a map of spaces by UUID
|
||||
spaces.forEach((space) => {
|
||||
map.set(
|
||||
space.uuid,
|
||||
this.spaceRepository.create({
|
||||
...space,
|
||||
children: [], // Add children if needed
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// Step 2: Organize the hierarchy
|
||||
const rootSpaces: SpaceEntity[] = [];
|
||||
spaces.forEach((space) => {
|
||||
if (space.parent && space.parent.uuid) {
|
||||
const parent = map.get(space.parent.uuid);
|
||||
parent?.children?.push(map.get(space.uuid));
|
||||
} else {
|
||||
rootSpaces.push(map.get(space.uuid));
|
||||
}
|
||||
});
|
||||
|
||||
return rootSpaces; // Return the root spaces with children nested within them
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,26 @@ import { SpaceRepositoryModule } from '@app/common/modules/space/space.repositor
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { SpaceController } from './controllers';
|
||||
import { SpaceService } from './services';
|
||||
import { SpaceService, SpaceUserService } from './services';
|
||||
import { SpaceRepository } from '@app/common/modules/space/repositories';
|
||||
import { CommunityRepository } from '@app/common/modules/community/repositories';
|
||||
import { SpaceUserController } from './controllers/space-user.controller';
|
||||
import {
|
||||
UserRepository,
|
||||
UserSpaceRepository,
|
||||
} from '@app/common/modules/user/repositories';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, SpaceRepositoryModule],
|
||||
controllers: [SpaceController],
|
||||
providers: [SpaceService, SpaceRepository, CommunityRepository],
|
||||
controllers: [SpaceController, SpaceUserController],
|
||||
providers: [
|
||||
SpaceService,
|
||||
SpaceRepository,
|
||||
CommunityRepository,
|
||||
UserSpaceRepository,
|
||||
UserRepository,
|
||||
SpaceUserService,
|
||||
],
|
||||
exports: [SpaceService],
|
||||
})
|
||||
export class SpaceModule {}
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './user.controller';
|
||||
export * from './user-communities.controller';
|
||||
export * from './user-space.controller';
|
30
src/users/controllers/user-space.controller.ts
Normal file
30
src/users/controllers/user-space.controller.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { ControllerRoute } from '@app/common/constants/controller-route';
|
||||
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
|
||||
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { UserSpaceService } from '../services';
|
||||
import { JwtAuthGuard } from '@app/common/guards/jwt.auth.guard';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
import { UserParamDto } from '../dtos';
|
||||
|
||||
@ApiTags('User Module')
|
||||
@Controller({
|
||||
version: EnableDisableStatusEnum.ENABLED,
|
||||
path: ControllerRoute.USER_SPACE.ROUTE,
|
||||
})
|
||||
export class UserSpaceController {
|
||||
constructor(private readonly userSpaceService: UserSpaceService) {}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: ControllerRoute.USER_SPACE.ACTIONS.GET_USER_SPACES_SUMMARY,
|
||||
description: ControllerRoute.USER_SPACE.ACTIONS.GET_USER_SPACES_DESCRIPTION,
|
||||
})
|
||||
async getSpacesForUser(
|
||||
@Param() params: UserParamDto,
|
||||
): Promise<BaseResponseDto> {
|
||||
return this.userSpaceService.getSpacesForUser(params.userUuid);
|
||||
}
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export * from './user.service';
|
||||
export * from './user-community.service';
|
||||
export * from './user-space.service';
|
||||
|
30
src/users/services/user-space.service.ts
Normal file
30
src/users/services/user-space.service.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { UserSpaceRepository } from '@app/common/modules/user/repositories';
|
||||
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
|
||||
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UserSpaceService {
|
||||
constructor(private readonly userSpaceRepository: UserSpaceRepository) {}
|
||||
|
||||
async getSpacesForUser(userUuid: string): Promise<BaseResponseDto> {
|
||||
const userSpaces = await this.userSpaceRepository.find({
|
||||
where: { user: { uuid: userUuid } },
|
||||
relations: ['space'],
|
||||
});
|
||||
|
||||
if (!userSpaces || userSpaces.length === 0) {
|
||||
throw new HttpException(
|
||||
`No spaces found for user with ID ${userUuid}`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const spaces = userSpaces.map((userSpace) => userSpace.space);
|
||||
|
||||
return new SuccessResponseDto({
|
||||
data: spaces,
|
||||
message: `Spaces for user ${userUuid} retrieved successfully`,
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user