diff --git a/.gitignore b/.gitignore index 70da15979ca0b33383ef01af9aee452eab36247f..89ec46735ca3b06e89627601da6563b7e8095067 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,7 @@ schema.sql build .npm -.config \ No newline at end of file +.config + +# VScode settings +.vscode/settings.json diff --git a/src/app.ts b/src/app.ts index fe6b0c92c86ed7215a76018c74664ace5e602284..31c5d9254643a793c91c24bbcbff9c70ade624f2 100644 --- a/src/app.ts +++ b/src/app.ts @@ -9,17 +9,19 @@ import { AuthorizationChecker } from './middleware/authorizationChecker'; import { Configuration } from './config/configuration'; import * as sentry from './log/sentry'; import * as swaggerUiExpress from 'swagger-ui-express'; -import { DevicesController } from './controllers/devices/controller'; import { NotificationsController } from './controllers/notifications/controller'; +import { DevicesController } from './controllers/devices/controller'; +import { ChannelsController } from './controllers/channels/controller'; import * as fs from 'fs'; import * as https from 'https'; Configuration.load(); -import defaultMetadataStorage from 'class-transformer/cjs/storage'; +// Note https://github.com/typestack/class-transformer/issues/563 +import { defaultMetadataStorage as classTransformerDefaultMetadataStorage } from 'class-transformer/cjs/storage'; const routingControllersOptions = { - controllers: [DevicesController, NotificationsController], + controllers: [DevicesController, NotificationsController, ChannelsController], }; // Check ROLES are defined in ENV for controller Authorizations @@ -56,7 +58,7 @@ let server; // Parse class-validator classes into JSON Schema: const schemas = validationMetadatasToSchemas({ - classTransformerMetadataStorage: defaultMetadataStorage, + classTransformerMetadataStorage: classTransformerDefaultMetadataStorage, refPointerPrefix: '#/components/schemas/', }); diff --git a/src/controllers/channels-controller.ts b/src/controllers/channels-controller.ts deleted file mode 100644 index ec308adae8bfbb79b2af314496eac8345ed4976c..0000000000000000000000000000000000000000 --- a/src/controllers/channels-controller.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { - JsonController, - Post, - BodyParam, - Get, - Req, - Authorized, - Param, - QueryParams, - Put, - Delete, -} from 'routing-controllers'; -import { ServiceFactory } from '../services/services-factory'; -import { ChannelsService } from '../services/channels-service'; -import { Group } from '../models/group'; -import { NotificationsService } from '../services/notifications-service'; -import { Category } from '../models/category'; -import { Tag } from '../models/tag'; - -@JsonController('/channels') -export class ChannelsController { - channelsService: ChannelsService = ServiceFactory.getChannelsService(); - notificationsService: NotificationsService = - ServiceFactory.getNotificationsService(); - - @Get() - @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) - async getAllChannels(@Req() req, @QueryParams() query) { - Object.keys(query).forEach( - key => query[key] === undefined && delete query[key], - ); - return this.channelsService.getAllChannels(query, req.authorizationBag); - } - - @Get('/:id') - @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) - async getChannelById(@Param('id') channelId: string, @Req() req) { - return this.channelsService.getChannelById(channelId, req.authorizationBag); - } - - @Get('/:id/edit') - @Authorized([process.env.INTERNAL_ROLE]) - async editChannelById(@Param('id') channelId: string, @Req() req) { - return this.channelsService.editChannelById( - channelId, - req.authorizationBag, - ); - } - - @Post() - @Authorized([process.env.INTERNAL_ROLE]) - async createChannel(@BodyParam('channel') channel, @Req() req) { - return this.channelsService.createChannel(channel, req.authorizationBag); - } - - @Delete('/:id') - @Authorized([process.env.INTERNAL_ROLE]) - async deleteChannel(@Param('id') channelId: string, @Req() req) { - return this.channelsService.deleteChannel(channelId, req.authorizationBag); - } - - @Put() - @Authorized([process.env.INTERNAL_ROLE]) - async updateChannel(@BodyParam('channel') channel, @Req() req) { - return this.channelsService.updateChannel(channel, req.authorizationBag); - } - - @Get('/:id/members') - @Authorized([process.env.INTERNAL_ROLE]) - async getChannelMembers( - @Param('id') channelId: string, - @QueryParams() query, - @Req() req, - ) { - Object.keys(query).forEach( - key => query[key] === undefined && delete query[key], - ); - - return this.channelsService.getChannelMembers( - channelId, - query, - req.authorizationBag, - ); - } - - @Put('/:id/members') - @Authorized([process.env.INTERNAL_ROLE]) - async addMemberToChannel( - @Param('id') channelId: string, - @BodyParam('username') username: string, - @Req() req, - ) { - return this.channelsService.addMemberToChannel( - username, - channelId, - req.authorizationBag, - ); - } - - @Delete('/:id/members') - @Authorized([process.env.INTERNAL_ROLE]) - async removeMemberFromChannel( - @Param('id') channelId: string, - @BodyParam('userId') memberId: string, - @Req() req, - ) { - return this.channelsService.removeMemberFromChannel( - memberId, - channelId, - req.authorizationBag, - ); - } - - @Get('/:id/groups') - @Authorized([process.env.INTERNAL_ROLE]) - async getChannelGroups( - @Param('id') channelId: string, - @QueryParams() query, - @Req() req, - ) { - Object.keys(query).forEach( - key => query[key] === undefined && delete query[key], - ); - return this.channelsService.getChannelGroups( - channelId, - query, - req.authorizationBag, - ); - } - - @Put('/:id/groups') - @Authorized([process.env.INTERNAL_ROLE]) - async addGroupToChannel( - @BodyParam('group') group: Group, - @Param('id') channelId: string, - @Req() req, - ) { - return this.channelsService.addGroupToChannel( - new Group(group), - channelId, - req.authorizationBag, - ); - } - - @Delete('/:id/groups') - @Authorized([process.env.INTERNAL_ROLE]) - async removeGroupFromChannel( - @Param('id') channelId: string, - @BodyParam('groupId') groupId: string, - @Req() req, - ) { - return this.channelsService.removeGroupFromChannel( - groupId, - channelId, - req.authorizationBag, - ); - } - - @Put('/:id/admingroup') - @Authorized([process.env.INTERNAL_ROLE]) - async addAdminGroupToChannel( - @BodyParam('group') group: Group, - @Param('id') id: string, - @Req() req, - ) { - return this.channelsService.updateChannelAdminGroup( - new Group(group), - id, - req.authorizationBag, - ); - } - - @Put('/:id/subscribe') - @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) - async subscribeToChannel(@Req() req, @Param('id') channelId: string) { - return this.channelsService.subscribeToChannel( - channelId, - req.authorizationBag, - ); - } - - @Put('/:id/unsubscribe') - @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) - async unsubscribeFromChannel(@Req() req, @Param('id') channelId: string) { - return this.channelsService.unsubscribeFromChannel( - channelId, - req.authorizationBag, - ); - } - - @Put('/:id/apikey') - @Authorized([process.env.INTERNAL_ROLE]) - async generateApiKey(@Req() req, @Param('id') channelId: string) { - return this.channelsService.generateApiKey(channelId, req.authorizationBag); - } - - @Put('/:id/category') - @Authorized([process.env.INTERNAL_ROLE]) - async setCategory( - @Req() req, - @Param('id') channelId: string, - @BodyParam('category') category: Category, - ) { - return this.channelsService.setCategory( - channelId, - category, - req.authorizationBag, - ); - } - - @Put('/:id/tags') - @Authorized([process.env.INTERNAL_ROLE]) - async setTags( - @Req() req, - @Param('id') channelId: string, - @BodyParam('tags') tags: Tag[], - ) { - return this.channelsService.setTags(channelId, tags, req.authorizationBag); - } - - @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) - @Get('/:id/notifications') - async getNotifications( - @Req() req, - @Param('id') channelId: string, - @QueryParams() query, - ) { - Object.keys(query).forEach( - key => query[key] === undefined && delete query[key], - ); - - return this.notificationsService.findAllNotifications( - channelId, - query, - req.authorizationBag, - ); - } - - @Put('/:id/owner') - @Authorized([process.env.INTERNAL_ROLE, process.env.SUPPORTER_ROLE]) - async setChannelOwner( - @Param('id') channelId: string, - @BodyParam('username') username: string, - @Req() req, - ) { - return this.channelsService.setChannelOwner( - username, - channelId, - req.authorizationBag, - ); - } - - @Get('/:id/stats') - @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) - async getChannelStats(@Param('id') channelId: string, @Req() req) { - return this.channelsService.getChannelStats( - channelId, - req.authorizationBag, - ); - } -} diff --git a/src/controllers/channels/controller.ts b/src/controllers/channels/controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..7fe6f1521fcc1bcf3fa6d0b44a5390b7d1bbdeca --- /dev/null +++ b/src/controllers/channels/controller.ts @@ -0,0 +1,472 @@ +import { + JsonController, + Post, + BodyParam, + Get, + Req, + Authorized, + Param, + QueryParams, + Put, + Delete, + Body, +} from 'routing-controllers'; +import { Category } from '../../models/category'; +import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; +import { ServiceFactory } from '../../services/services-factory'; +import { ChannelsService } from '../../services/channels-service'; +import { NotificationsService } from '../../services/notifications-service'; +import { + ChannelResponse, + ChannelsListResponse, + CreateChannelRequest, + UpdateChannelRequest, + ChannelsQuery, + Query, + MembersListResponse, + GroupsListResponse, + GroupResponse, + MemberResponse, + GetChannelResponse, + EditChannelResponse, + NotificationsListResponse, + ChannelStatsResponse, + setTagsRequest, +} from './dto'; +import { StatusCodeDescriptions, StatusCodes } from '../../utils/status-codes'; + +@JsonController('/channels') +export class ChannelsController { + channelsService: ChannelsService = ServiceFactory.getChannelsService(); + notificationsService: NotificationsService = ServiceFactory.getNotificationsService(); + + @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) + @OpenAPI({ + summary: 'Lists all of the channels matching the query.', + description: 'Lists all channels.', + operationId: 'listChannels', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(ChannelsListResponse, { description: 'List of queried channels.' }) + @Get() + async getAllChannels(@Req() req, @QueryParams() query: ChannelsQuery): Promise<ChannelsListResponse> { + Object.keys(query).forEach(key => query[key] === undefined && delete query[key]); + return this.channelsService.getAllChannels(query, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) + @OpenAPI({ + summary: 'Returns the channel with the provided channel id.', + description: 'Returns a channel.', + operationId: 'getChannelById', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(GetChannelResponse, { description: 'Requested channel.' }) + @Get('/:id') + async getChannelById(@Param('id') channelId: string, @Req() req): Promise<GetChannelResponse> { + return await this.channelsService.getChannelById(channelId, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) + @OpenAPI({ + summary: 'Fetches all channel information for the settings page.', + description: 'Fetches channel information.', + operationId: 'getChannelEdit', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(EditChannelResponse, { description: "Requested channel's full settings page information." }) + @Get('/:id/edit') + async editChannelById(@Param('id') channelId: string, @Req() req): Promise<EditChannelResponse> { + return this.channelsService.editChannelById(channelId, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: 'Creates a new channel.', + description: 'Creates a new channel.', + operationId: 'createChannel', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(ChannelResponse, { description: 'Channel created json object.' }) + @Post() + async createChannel(@Body() channel: CreateChannelRequest, @Req() req): Promise<ChannelResponse> { + return this.channelsService.createChannel(channel, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: 'Deletes a channel.', + description: 'Deletes the channel with the provided channel id.', + operationId: 'deleteChannel', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(String) + @Delete('/:id') + //@OnUndefined(204) should we be using this and not return anything + async deleteChannel(@Param('id') channelId: string, @Req() req): Promise<string> { + return this.channelsService.deleteChannel(channelId, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: 'Updates a channel.', + description: 'Updates a channel with the provided information.', + operationId: 'updateChannel', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(ChannelResponse, { description: 'Updated channel json object.' }) + @Put() + async updateChannel(@BodyParam('channel') channel: UpdateChannelRequest, @Req() req): Promise<ChannelResponse> { + return this.channelsService.updateChannel(channel, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: "Fetches a channel's members.", + description: 'Fetches the queried members from the channel with the provided channel id.', + operationId: 'listChannelMembers', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(MembersListResponse, { description: 'Channel member list json object.' }) + @Get('/:id/members') + async getChannelMembers( + @Param('id') channelId: string, + @QueryParams() query: Query, + @Req() req, + ): Promise<MembersListResponse> { + Object.keys(query).forEach(key => query[key] === undefined && delete query[key]); + return this.channelsService.getChannelMembers(channelId, query, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: 'Adds a member to a channel.', + description: 'Adds a member with the provided username to the channel with the provided channel id.', + operationId: 'addMemberToChannel', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(MemberResponse, { description: 'Added member json object.' }) + @Post('/:id/members/:username') + async addMemberToChannel( + @Param('id') channelId: string, + @Param('username') username: string, + @Req() req, + ): Promise<MemberResponse> { + return this.channelsService.addMemberToChannel(username, channelId, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: 'Removes a member from a channel.', + description: 'Removes a member with the provided username from the channel with the provided channel id.', + operationId: 'removeMemberFromChannel', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(String, { description: 'Removed member id' }) + @Delete('/:id/members/:userId') + async removeMemberFromChannel( + @Param('id') channelId: string, + @Param('userId') memberId: string, + @Req() req, + ): Promise<string> { + return this.channelsService.removeMemberFromChannel(memberId, channelId, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: "Fetches a channel's member groups.", + description: + 'Fetches the member groups of the channel with the provided channel id, according to the query parameters.', + operationId: 'listChannelGroups', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(GroupsListResponse, { description: 'List of channel member groups.' }) + @Get('/:id/groups') + async getChannelGroups( + @Param('id') channelId: string, + @QueryParams() query: Query, + @Req() req, + ): Promise<GroupsListResponse> { + Object.keys(query).forEach(key => query[key] === undefined && delete query[key]); + return this.channelsService.getChannelGroups(channelId, query, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: 'Adds a group to a channel.', + description: 'Adds a group with the provided group name to the channel with the provided channel id.', + operationId: 'addGroupToChannel', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(GroupResponse, { description: 'Group added to the channel as member.' }) + @Put('/:id/groups/:groupname') + async addGroupToChannel(@Param('id') channelId: string, @Param('groupname') groupName: string, @Req() req) { + return this.channelsService.addGroupToChannel(groupName, channelId, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: "Removes a channel's member group.", + description: 'Removes the member group with the provided group id from the channel with the provided channel id.', + operationId: 'removeGroupFromChannel', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(String, { description: 'Removed group id.' }) + @Delete('/:id/groups/:groupid') + async removeGroupFromChannel(@Param('id') channelId: string, @Param('groupid') groupId: string, @Req() req) { + return this.channelsService.removeGroupFromChannel(groupId, channelId, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: "Sets a group as channel's admin group.", + description: "Sets the channel's admin group to the group with the provided group name.", + operationId: 'updateChannelAdminGroup', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(ChannelResponse, { description: 'Changed channel information.' }) + @Put('/:id/admingroup/:group') + async addAdminGroupToChannel(@Param('id') id: string, @Param('group') groupName: string, @Req() req) { + return this.channelsService.updateChannelAdminGroup(groupName, id, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) + @OpenAPI({ + summary: 'Subscribes the caller to the channel.', + description: 'Subscribes the endpoint-calling user to the channel with the provided channel id.', + operationId: 'subscribeToChannel', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(ChannelResponse, { description: 'Channel after new user subscribed.' }) + @Put('/:id/subscribe') + async subscribeToChannel(@Req() req, @Param('id') channelId: string) { + return this.channelsService.subscribeToChannel(channelId, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) + @OpenAPI({ + summary: 'Unsubscribes the caller from the channel.', + description: 'Unsubscribes the endpoint-calling user from the channel with the provided channel id.', + operationId: 'unsubcribeFromChannel', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(ChannelResponse, { description: 'Channel after new user subscribed.' }) + @Put('/:id/unsubscribe') + async unsubscribeFromChannel(@Req() req, @Param('id') channelId: string) { + return this.channelsService.unsubscribeFromChannel(channelId, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: 'Generates an API Key for the channel.', + description: 'Generates an API Key belonging to the channel with the provided channel id.', + operationId: 'generateAPIKey', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(ChannelResponse, { description: 'Channel after API Key generation.' }) + @Put('/:id/apikey') + async generateApiKey(@Req() req, @Param('id') channelId: string) { + return this.channelsService.generateApiKey(channelId, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: 'Sets a category for the channel.', + description: 'Sets the provided category, on the channel with the provided channel id.', + operationId: 'setChannelCategory', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(ChannelResponse, { description: 'Channel after setting a category.' }) + @Put('/:id/category') + async setCategory(@Req() req, @Param('id') channelId: string, @BodyParam('category') category: Category) { + return this.channelsService.setCategory(channelId, category, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE]) + @OpenAPI({ + summary: 'Sets tags for the channel.', + description: 'Sets the provided tags, on the channel with the provided channel id.', + operationId: 'setChannelTags', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(ChannelResponse, { description: 'Channel after setting tags.' }) + @Put('/:id/tags') + async setTags(@Req() req, @Param('id') channelId: string, @Body() tags: setTagsRequest): Promise<ChannelResponse> { + return this.channelsService.setTags(channelId, tags, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) + @OpenAPI({ + summary: "Fetches the channel's notifications.", + description: + 'Fetches the notifications of the channel with the provided channel id, according to the query parameters.', + operationId: 'listChannelNotifications', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(NotificationsListResponse, { description: 'List of notifications on the channel and their count.' }) + @Get('/:id/notifications') + async getNotifications( + @Req() req, + @Param('id') channelId: string, + @QueryParams() query: Query, + ): Promise<NotificationsListResponse> { + Object.keys(query).forEach(key => query[key] === undefined && delete query[key]); + return this.notificationsService.findAllNotifications(channelId, query, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE, process.env.SUPPORTER_ROLE]) + @OpenAPI({ + summary: "Sets the channel's owner.", + description: 'Sets the owner of the channel with the provided channel id, to the user with the provided username.', + operationId: 'setChannelOwner', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(ChannelResponse, { description: 'Channel after setting the owner.' }) + @Put('/:id/owner') + async setChannelOwner( + @Param('id') channelId: string, + @BodyParam('username') username: string, + @Req() req, + ): Promise<ChannelResponse> { + return this.channelsService.setChannelOwner(username, channelId, req.authorizationBag); + } + + @Authorized([process.env.INTERNAL_ROLE, process.env.VIEWER_ROLE]) + @OpenAPI({ + summary: "Gets the channel's stats.", + description: 'Gets the stats of the channel with the provided channel id.', + operationId: 'getChannelStats', + security: [{ oauth2: [] }], + responses: { + [StatusCodes.OK]: StatusCodeDescriptions[StatusCodes.OK], + [StatusCodes.BadRequest]: StatusCodeDescriptions[StatusCodes.BadRequest], + [StatusCodes.Unauthorized]: StatusCodeDescriptions[StatusCodes.Unauthorized], + [StatusCodes.Forbidden]: StatusCodeDescriptions[StatusCodes.Forbidden], + }, + }) + @ResponseSchema(ChannelStatsResponse, { description: 'Channel stats json object.' }) + @Get('/:id/stats') + async getChannelStats(@Param('id') channelId: string, @Req() req): Promise<ChannelStatsResponse> { + return this.channelsService.getChannelStats(channelId, req.authorizationBag); + } +} diff --git a/src/controllers/channels/dto.ts b/src/controllers/channels/dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..532923627afbe8a2d6e17b1c91d1252826855acc --- /dev/null +++ b/src/controllers/channels/dto.ts @@ -0,0 +1,1645 @@ +import { + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, + IsBoolean, + IsEnum, + IsEmail, + IsNumber, + MinLength, + MaxLength, + IsUUID, + Min, + IsDateString, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { JSONSchema } from 'class-validator-jsonschema'; +import { ChannelFlags, SubmissionByEmail, SubmissionByForm, Visibility } from '../../models/channel-enums'; +import { Notification } from '../../models/notification'; +import { Channel } from '../../models/channel'; +import { Group } from '../../models/group'; +import { User } from '../../models/user'; +import { AuthorizationBag } from '../../models/authorization-bag'; +import { v4 } from 'uuid'; + +export class CategoryResponse { + @IsString() + id: string; + + @IsString() + name: string; +} + +export class Tag { + @IsString() + id: string; + + @IsString() + name: string; +} + +@JSONSchema({ + description: 'Member groups query parameters.', + example: { + take: 10, + skip: 20, + searchText: 'groupname', + }, +}) +export class Query { + @JSONSchema({ + description: 'For pagination, number of items to skip in result. Default: 10.', + }) + @IsOptional() + @IsNumber() + take: number; + + @JSONSchema({ + description: 'For pagination, number of items to skip in result. Default: 0.', + }) + @IsOptional() + @IsNumber() + skip: number; + + @JSONSchema({ + description: 'Search text to be used.', + }) + @IsOptional() + @IsString() + searchText: string; +} + +@JSONSchema({ + description: 'New Device json input.', + example: { + take: 10, + skip: 20, + searchText: 'ITMM minutes', + ownerFilter: false, + subscribedFilter: true, + favoritesFilter: false, + tags: ['News', 'Announcements'], + category: 'CDA', + }, +}) +export class ChannelsQuery extends Query { + @JSONSchema({ + description: 'If channels returned must be owned by the user.', + example: false, + }) + @IsBoolean() + ownerFilter: boolean; + + @JSONSchema({ + description: 'If channels returned must be subscribed to by the user.', + example: false, + }) + @IsBoolean() + subscribedFilter: boolean; + + @JSONSchema({ + description: 'If channels returned must be favorited by the user.', + example: false, + }) + @IsBoolean() + favoritesFilter: boolean; + + @JSONSchema({ + description: 'Tags that the returned channels must have.', + }) + @IsOptional() + @IsString({ each: true }) + tags: string[]; + + @JSONSchema({ + description: 'Category that the returned channels must have.', + }) + @IsOptional() + @IsString() + category: string; +} + +@JSONSchema({ + description: 'List of groups.', + example: { + groups: [ + { + id: v4(), + groupIdentifier: 'group1name', + }, + { + id: v4(), + groupIdentifier: 'group2name', + }, + ], + totalNumberOfGroups: 2, + }, +}) +export class GroupsListResponse { + @JSONSchema({ + description: 'List of groups.', + example: [ + { + id: v4(), + groupIdentifier: 'group1name', + }, + { + id: v4(), + groupIdentifier: 'group2name', + }, + ], + }) + @ValidateNested({ each: true }) + @Type(() => GroupResponse) + groups: GroupResponse[]; + + @JSONSchema({ + description: 'Number of groups in the list.', + example: 2, + }) + @IsNumber() + totalNumberOfGroups: number; + + constructor(list: GroupResponse[]) { + this.groups = list; + this.totalNumberOfGroups = this.groups.length; + } +} + +@JSONSchema({ + description: 'List of channel members json object.', + example: { + members: [ + { + id: v4(), + username: 'member1username', + email: 'member1email', + }, + { + id: v4(), + username: 'member2username', + email: 'member2email', + }, + ], + totalNumberOfMembers: 2, + }, +}) +export class MembersListResponse { + @ValidateNested({ each: true }) + @Type(() => MemberResponse) + @JSONSchema({ + description: 'Members list.', + example: [ + { + id: v4(), + username: 'member1username', + email: 'member1email', + }, + { + id: v4(), + username: 'member2username', + email: 'member2email', + }, + ], + }) + members: MemberResponse[]; + + @IsNumber() + @JSONSchema({ + description: 'Number of channel members returned.', + example: 2, + }) + totalNumberOfMembers: number; + + constructor(list: User[] = []) { + this.members = list.map(u => new MemberResponse(u)); + this.totalNumberOfMembers = this.members.length; + } +} + +@JSONSchema({ + description: 'List of notifications json object.', + example: 2, +}) +export class NotificationsListResponse { + @ValidateNested({ each: true }) + @Type(() => Notification) + @JSONSchema({ + description: 'List of notifications.', + example: [{}], + }) + items: Notification[]; //would be NotificationResponse, but wait for other MR to be merged + + @JSONSchema({ + description: 'Number of notifications returned.', + example: 2, + }) + @IsNumber() + count: number; + + constructor(list: Notification[] = []) { + this.items = list; + this.count = this.items.length; + } +} + +@JSONSchema({ + description: 'Channel member json object.', + example: { + id: v4(), + username: 'memberusername', + email: 'memberemail', + }, +}) +export class MembersResponse { + @IsString() + @IsUUID('4') + @JSONSchema({ + description: 'Channel member id.', + example: v4(), + }) + id: string; + + @IsString() + @IsNotEmpty() + @JSONSchema({ + description: 'Channel member username.', + example: 'memberusername', + }) + username: string; + + @IsString() + @IsEmail() + @JSONSchema({ + description: 'Channel member email.', + example: 'memberemail', + }) + email: string; + + constructor(member: User) { + this.id = member.id; + this.username = member.username; + this.email = member.email; + } +} + +export class GroupsResponse { + @IsNumber() + count: number; + + constructor(count: number) { + this.count = count; + } +} + +@JSONSchema({ + description: 'Channel stats object.', + example: { + id: v4(), + name: 'channelname', + members: 20, + unsubscribed: 2, + owner: { + id: v4(), + name: 'ownerusername', + }, + creationDate: new Date(), + lastActivityDate: new Date(), + groups: 3, + }, +}) +export class ChannelStatsResponse { + @IsString() + @IsUUID('4') + @JSONSchema({ + description: 'Channel id.', + example: v4(), + }) + id: string; + + @IsString() + @IsNotEmpty() + @JSONSchema({ + description: 'Channel name.', + example: 'channelname', + }) + name: string; + + @ValidateNested({ each: true }) + @Type(() => User) + @JSONSchema({ + description: 'Channel members.', + example: 20, + }) + members: number; + + @IsNumber() + @Min(0) + @JSONSchema({ + description: 'Number of unsubscribed channel members.', + example: 2, + }) + unsubscribed: number; + + @Type(() => User) + @JSONSchema({ + description: 'Channel owner.', + example: { + id: v4(), + name: 'ownerusername', + }, + }) + owner: User; + + @IsDateString() + @IsNotEmpty() + @JSONSchema({ + description: 'The date the channel was created.', + example: new Date(), + }) + creationDate: Date; + + @IsDateString() + @IsNotEmpty() + @JSONSchema({ + description: 'The date the channel was last active.', + example: new Date(), + }) + lastActivityDate: Date; + + @IsNumber() + @Min(0) + @JSONSchema({ + description: 'Number of channel member groups.', + example: 3, + }) + groups: number; + + constructor(channel: Channel) { + this.id = channel.id; + this.members = channel.members.length; + this.unsubscribed = channel.unsubscribed.length; + this.owner = channel.owner; + this.creationDate = channel.creationDate; + this.lastActivityDate = channel.lastActivityDate; + this.groups = channel.groups.length; + } +} + +export class Relationship { + @Type(() => NotificationsListResponse) + notifications: NotificationsListResponse; + + members: MembersListResponse; + + groups: GroupsResponse; + + constructor(notif: NotificationsListResponse, members: MembersListResponse, groups: GroupsResponse) { + this.notifications = notif; + this.members = members; + this.groups = groups; + } +} +export class ListChannelsFilterObject { + @IsNotEmpty() + @IsString() + id: string; + + @IsNotEmpty() + @IsString() + slug: string; + + @IsNotEmpty() + @IsString() + @MaxLength(256) + description: string; + + @IsNotEmpty() + @IsString() + @MinLength(4) + @MaxLength(128) + name: string; + + @IsNotEmpty() + @IsEnum(Visibility) + visibility: Visibility; + + @IsBoolean() + archive: boolean; + + @IsBoolean() + subscribed: boolean; + + @IsBoolean() + @IsOptional() + manage: boolean; + + @IsBoolean() + @IsOptional() + send: boolean; + + @Type(() => CategoryResponse) + @IsOptional() + category: CategoryResponse; + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => Tag) + tags: Tag[]; + + @IsOptional() + @Type(() => Relationship) + relationships: Relationship; + + @IsEnum(ChannelFlags, { each: true }) + @JSONSchema({ + description: 'List of labels related to the channel.', + example: [ChannelFlags.critical, ChannelFlags.mandatory], + }) + channelFlags: ChannelFlags[]; + + constructor(channel: Channel, subscribed: boolean, manage: boolean, send: boolean) { + this.id = channel.id; + this.slug = channel.slug; + this.description = channel.description; + this.name = channel.name; + this.visibility = channel.visibility; + this.archive = channel.archive; + this.subscribed = subscribed; + this.manage = manage; + this.send = send; + this.category = channel.category; + this.tags = channel.tags; + this.relationships = { + notifications: new NotificationsListResponse(channel.notifications), + members: new MembersListResponse(channel.members), + groups: { + count: channel.groups?.length || 0, + }, + }; + this.channelFlags = channel.channelFlags; + } +} + +@JSONSchema({ + description: 'Device json return.', + example: { + channels: [ + { + id: v4(), + slug: 'channel1-name', + description: 'channel1description', + name: 'channel1 name', + visibility: Visibility.restricted, + archive: true, + subscribed: true, + manage: true, + send: true, + category: null, + tags: [], + relationships: { + notifications: { + count: 73, + }, + members: { + count: 35, + }, + groups: { + count: 2, + }, + }, + }, + { + id: v4(), + slug: 'channel2-name', + description: 'channel2description', + name: 'channel2 name', + visibility: Visibility.restricted, + archive: true, + subscribed: true, + manage: true, + send: true, + category: null, + tags: [], + relationships: { + notifications: { + count: 13, + }, + members: { + count: 2, + }, + groups: { + count: 1, + }, + }, + }, + ], + count: 2, + }, +}) +export class ChannelsListResponse { + @JSONSchema({ + description: 'List of channels.', + example: [ + { + id: v4(), + slug: 'channel1-name', + description: 'channel1description', + name: 'channel1 name', + visibility: Visibility.restricted, + archive: true, + subscribed: true, + manage: true, + send: true, + category: null, + tags: [], + relationships: { + notifications: { + count: 73, + }, + members: { + count: 35, + }, + groups: { + count: 2, + }, + }, + }, + { + id: v4(), + slug: 'channel2-name', + description: 'channel2description', + name: 'channel2 name', + visibility: Visibility.restricted, + archive: true, + subscribed: true, + manage: true, + send: true, + category: null, + tags: [], + relationships: { + notifications: { + count: 13, + }, + members: { + count: 2, + }, + groups: { + count: 1, + }, + }, + }, + ], + }) + @ValidateNested({ each: true }) + @Type(() => ListChannelsFilterObject) + channels: ListChannelsFilterObject[]; + + @IsNumber() + @JSONSchema({ + description: 'Number of returned channels.', + example: 2, + }) + count: number; + + constructor(list: ListChannelsFilterObject[]) { + this.channels = list; + this.count = this.channels.length; + } +} + +@JSONSchema({ + description: 'Channel object json.', + example: { + id: v4(), + name: 'CERN Notifications Updates', + description: 'Service updates for CERN Notifications.', + category: { + name: 'CDA', + id: v4(), + }, + tags: [ + { + id: v4(), + name: 'Channel tag1', + }, + { + id: v4(), + name: 'Channel tag2', + }, + ], + send: true, + }, +}) +export class GetChannelResponse { + @IsNotEmpty() + @IsUUID('4') + @JSONSchema({ + description: 'Channel id.', + example: v4(), + }) + id: string; + + @IsNotEmpty() + @IsString() + @JSONSchema({ + description: 'Channel name.', + example: 'CERN Notifications Updates', + }) + name: string; + + @IsString() + @IsNotEmpty() + @JSONSchema({ + description: 'Channel description.', + example: 'Service updates for CERN Notifications.', + }) + description: string; + + @Type(() => CategoryResponse) + @IsOptional() + @JSONSchema({ + description: 'Channel category.', + example: { + name: 'CDA', + id: v4(), + }, + }) + category: CategoryResponse; + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => Tag) + @JSONSchema({ + description: 'Channel Tags.', + example: [ + { + id: v4(), + name: 'Channel tag1', + }, + { + id: v4(), + name: 'Channel tag2', + }, + ], + }) + tags: Tag[]; + + @IsBoolean() + @IsOptional() + @JSONSchema({ + description: 'User has send permission on the channel.', + example: true, + }) + send: boolean; + + @IsEnum(ChannelFlags, { each: true }) + @JSONSchema({ + description: 'List of labels related to the channel.', + example: [ChannelFlags.critical, ChannelFlags.mandatory], + }) + channelFlags: ChannelFlags[]; + + constructor(channel: Channel) { + this.id = channel.id; + this.name = channel.name; + this.description = channel.description; + this.category = channel.category; + this.tags = channel.tags; + this.channelFlags = channel.channelFlags; + } + + async setSendPermission(channel: Channel, authBag: AuthorizationBag): Promise<void> { + this.send = await channel.canSendByForm(authBag); + } +} + +@JSONSchema({ + description: 'Channel full settings page information.', + example: { + id: v4(), + name: 'CERN Notifications Updates', + slug: 'cern-notification-updates', + owner: { + id: v4(), + username: 'user', + email: 'useremail@cern.ch', + enabled: true, + created: new Date(), + lastLogin: new Date(), + }, + description: 'Service updates for CERN Notifications.', + adminGroup: { + id: v4(), + groupIdentifier: 'admingroupname', + }, + visibility: Visibility.public, + sendPrivate: false, + incomingEmail: 'someemail@cern.ch', + incomingEgroup: 'notifications-service-admins@cern.ch', + submissionByEmail: [ + SubmissionByEmail.administrators, + SubmissionByEmail.members, + SubmissionByEmail.email, + SubmissionByEmail.egroup, + ], + submissionByForm: [SubmissionByForm.administrators, SubmissionByForm.members, SubmissionByForm.apikey], + archive: true, + APIKey: 'ck_' + v4() + '_' + v4(), + tags: [ + { + id: v4(), + name: 'Channel tag1', + }, + { + id: v4(), + name: 'Channel tag2', + }, + ], + category: { + id: v4(), + name: 'CDA', + }, + }, +}) +export class EditChannelResponse { + @IsNotEmpty() + @IsUUID('4') + @JSONSchema({ + description: 'Channel id.', + example: v4(), + }) + id: string; + + @IsNotEmpty() + @IsString() + @JSONSchema({ + description: 'Channel name.', + example: 'CERN Notifications Updates', + }) + name: string; + + @IsNotEmpty() + @IsString() + @JSONSchema({ + description: 'Channel slug. Usually a slugified version of the name, eg channel-name.', + example: 'cern-notification-updates', + }) + slug: string; + + @IsNotEmpty() + @JSONSchema({ + description: 'Channel owner.', + example: { + id: v4(), + username: 'user', + email: 'useremail@cern.ch', + enabled: true, + created: new Date(), + lastLogin: new Date(), + }, + }) + owner: User; + + @IsString() + @JSONSchema({ + description: 'Channel description.', + example: 'Service updates for CERN Notifications.', + }) + description: string; + + @IsOptional() + @JSONSchema({ + description: 'Channel admin group.', + example: { + id: v4(), + groupIdentifier: 'admingroupname', + }, + }) + adminGroup: Group; + + @IsNotEmpty() + @IsEnum(Visibility) + @JSONSchema({ + description: 'Channel visibility.', + example: Visibility.public, + }) + visibility: Visibility; + + @IsBoolean() + @JSONSchema({ + description: "The channel's setting for allowing private notifications.", + example: false, + }) + sendPrivate: boolean; + + @IsString() + @JSONSchema({ + description: 'Emails which can send notifications by email to the channel.', + example: 'someemail@cern.ch', + }) + incomingEmail: string; + + @IsString() + @JSONSchema({ + description: 'Egroup which can send notifications by email to the channel.', + example: 'notifications-service-admins@cern.ch', + }) + incomingEgroup: string; + + @IsEnum(SubmissionByEmail, { each: true }) + @JSONSchema({ + description: 'Determines who and how can send notifications by email.', + example: [ + SubmissionByEmail.administrators, + SubmissionByEmail.members, + SubmissionByEmail.email, + SubmissionByEmail.egroup, + ], + }) + submissionByEmail: SubmissionByEmail[]; + + @IsEnum(SubmissionByForm, { each: true }) + @JSONSchema({ + description: 'Who can send notifications via Web or API. Default is ADMINISTRATORS.', + example: [SubmissionByForm.administrators, SubmissionByForm.members, SubmissionByForm.apikey], + }) + submissionByForm: SubmissionByForm[]; + + @IsBoolean() + @JSONSchema({ + description: + "Allows enabling preservation of the channel's notifications into an external archive. Notifications are deleted from our system (not the archive) after 13 months in any case.", + example: true, + }) + archive: boolean; + + @IsString() + @JSONSchema({ + description: 'API Key.', + example: 'ck_' + v4() + '_' + v4(), + }) + APIKey: string; + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => Tag) + @JSONSchema({ + description: "The channel's Tags.", + example: [ + { + id: v4(), + name: 'Channel tag1', + }, + { + id: v4(), + name: 'Channel tag2', + }, + ], + }) + tags: Tag[]; + + @Type(() => CategoryResponse) + @IsOptional() + @JSONSchema({ + description: "The channel's Category.", + example: { + id: v4(), + name: 'CDA', + }, + }) + category: CategoryResponse; + + constructor(channel: Channel) { + this.id = channel.id; + this.name = channel.name; + this.slug = channel.slug; + this.owner = channel.owner; + this.description = channel.description; + this.adminGroup = channel.adminGroup; + this.visibility = channel.visibility; + this.sendPrivate = channel.sendPrivate; + this.incomingEmail = channel.incomingEmail; + this.incomingEgroup = channel.incomingEgroup; + this.submissionByEmail = channel.submissionByEmail; + this.submissionByForm = channel.submissionByForm; + this.archive = channel.archive; + this.tags = channel.tags; + this.category = channel.category; + this.APIKey = channel.APIKey; + } +} + +@JSONSchema({ + description: 'Channel response json object.', + example: { + id: v4(), + name: 'CERN Notifications Updates', + slug: 'cern-notification-updates', + owner: { + id: v4(), + username: 'user', + email: 'useremail@cern.ch', + enabled: true, + created: new Date(), + lastLogin: new Date(), + }, + description: 'Service updates for CERN Notifications.', + members: [ + { + id: v4(), + username: 'user1', + email: 'user1email@cern.ch', + enabled: true, + created: new Date(), + lastLogin: new Date(), + }, + { + id: v4(), + username: 'user2', + email: 'user2email@cern.ch', + enabled: true, + created: new Date(), + lastLogin: new Date(), + }, + ], + adminGroup: { + id: v4(), + groupIdentifier: 'admingroupname', + }, + unsubscribed: [ + { + id: v4(), + username: 'user3', + email: 'user3email@cern.ch', + enabled: true, + created: new Date(), + lastLogin: new Date(), + }, + { + id: v4(), + username: 'user4', + email: 'user4email@cern.ch', + enabled: true, + created: new Date(), + lastLogin: new Date(), + }, + ], + groups: [ + { + id: v4(), + groupIdentifier: 'group1', + }, + { + id: v4(), + groupIdentifier: 'group2', + }, + ], + visibility: Visibility.public, + incomingEmail: 'someemail@cern.ch', + incomingEgroup: 'notifications-service-admins@cern.ch', + submissionByEmail: [ + SubmissionByEmail.administrators, + SubmissionByEmail.members, + SubmissionByEmail.email, + SubmissionByEmail.egroup, + ], + submissionByForm: [SubmissionByForm.administrators, SubmissionByForm.members, SubmissionByForm.apikey], + archive: true, + APIKey: 'ck_' + v4() + '_' + v4(), + sendPrivate: false, + creationDate: new Date(), + lastActivityDate: new Date(), + deleteDate: new Date(), + subscribed: true, + manage: true, + access: true, + send: true, + tags: [ + { + id: v4(), + name: 'Channel tag1', + }, + { + id: v4(), + name: 'Channel tag2', + }, + ], + category: { + id: v4(), + name: 'CDA', + }, + relationships: { + notifications: { + count: 13, + }, + members: { + count: 2, + }, + groups: { + count: 1, + }, + }, + }, +}) +export class ChannelResponse { + @IsNotEmpty() + @JSONSchema({ + description: "The channel's id.", + example: v4(), + }) + id: string; + + @IsNotEmpty() + @IsString() + @JSONSchema({ + description: 'Channel slug. Usually a slugified version of the name, eg channel-name.', + example: 'channelname', + }) + slug: string; + + @IsNotEmpty() + @JSONSchema({ + description: "The channel's owner.", + example: { + id: v4(), + username: 'user', + email: 'useremail@cern.ch', + enabled: true, + created: new Date(), + lastLogin: new Date(), + }, + }) + owner: User; + + @IsNotEmpty() + @IsString() + @JSONSchema({ + description: "The channel's name.", + example: 'Channel name', + }) + name: string; + + @IsString() + @JSONSchema({ + description: "The channel's description.", + example: 'This channel is not real.', + }) + description: string; + + @ValidateNested({ each: true }) + @Type(() => User) + @JSONSchema({ + description: "The channel's members.", + example: [ + { + id: v4(), + username: 'user1', + email: 'user1email@cern.ch', + enabled: true, + created: new Date(), + lastLogin: new Date(), + }, + { + id: v4(), + username: 'user2', + email: 'user2email@cern.ch', + enabled: true, + created: new Date(), + lastLogin: new Date(), + }, + ], + }) + members: User[]; + + @IsOptional() + @JSONSchema({ + description: "The channel's admin group.", + example: { + id: v4(), + groupIdentifier: 'admingroupname', + }, + }) + adminGroup: Group; + + @ValidateNested({ each: true }) + @Type(() => User) + @JSONSchema({ + description: "The channel's unsubscribed members.", + example: [ + { + id: v4(), + username: 'user3', + email: 'user3email@cern.ch', + enabled: true, + created: new Date(), + lastLogin: new Date(), + }, + { + id: v4(), + username: 'user4', + email: 'user4email@cern.ch', + enabled: true, + created: new Date(), + lastLogin: new Date(), + }, + ], + }) + unsubscribed: User[]; + + @ValidateNested({ each: true }) + @Type(() => Group) + @JSONSchema({ + description: "The channel's groups.", + example: [ + { + id: v4(), + groupIdentifier: 'group1', + }, + { + id: v4(), + groupIdentifier: 'group2', + }, + ], + }) + groups: Group[]; + + @IsNotEmpty() + @IsEnum(Visibility) + @JSONSchema({ + description: "The channel's visibility.", + example: Visibility.restricted, + }) + visibility: Visibility; + + @IsBoolean() + @JSONSchema({ + description: "The channel's archival option.", + example: true, + }) + archive: boolean; + + @IsString() + @JSONSchema({ + description: "The channel's APIKey.", + example: 'ck_' + v4() + '_' + v4(), + }) + APIKey: string; + + @IsBoolean() + @JSONSchema({ + description: "The channel's setting for allowing private notifications.", + example: false, + }) + sendPrivate: boolean; + + @IsDateString() + @JSONSchema({ + description: "The channel's creation date.", + example: new Date(), + }) + creationDate: Date; + + @IsDateString() + @JSONSchema({ + description: "The channel's last activity date.", + example: new Date(), + }) + lastActivityDate: Date; + + @IsEmail() + @JSONSchema({ + description: "The channel's incoming email for notifications.", + example: 'channelname+level@cern.ch', + }) + incomingEmail: string; + + @IsString() + @JSONSchema({ + description: "The channel's incoming egroup for notifications.", + example: 'incomingegroupname@cern.ch', + }) + incomingEgroup: string; + + @IsEnum(SubmissionByEmail, { each: true }) + @JSONSchema({ + description: 'Determines who and how can send notifications by email.', + example: [ + SubmissionByEmail.administrators, + SubmissionByEmail.members, + SubmissionByEmail.email, + SubmissionByEmail.egroup, + ], + }) + submissionByEmail: SubmissionByEmail[]; + + @IsEnum(SubmissionByForm, { each: true }) + @JSONSchema({ + description: 'Who can send notifications via Web or API. Default is ADMINISTRATORS.', + example: [SubmissionByForm.administrators, SubmissionByForm.members, SubmissionByForm.apikey], + }) + submissionByForm: SubmissionByForm[]; + + @IsOptional() + @IsDateString() + @JSONSchema({ + description: "The channel's deletion date.", + example: new Date(), + }) + deleteDate: Date; + + @IsBoolean() + @IsOptional() + @JSONSchema({ + description: 'Whether the user is subscribed to this channel.', + example: true, + }) + subscribed: boolean; + + @IsBoolean() + @IsOptional() + @JSONSchema({ + description: 'Whether the user can manage this channel.', + example: true, + }) + manage: boolean; + + @IsBoolean() + @IsOptional() + @JSONSchema({ + description: 'Whether the user can access this channel.', + example: true, + }) + access: boolean; + + @IsBoolean() + @IsOptional() + @JSONSchema({ + description: 'Whether the user can send notifications to this channel.', + example: true, + }) + send: boolean; + + @Type(() => CategoryResponse) + @IsOptional() + @JSONSchema({ + description: 'Channel category.', + example: { + name: 'CDA', + id: v4(), + }, + }) + category: CategoryResponse; + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => Tag) + @JSONSchema({ + description: "The channel's Tags.", + example: [ + { + id: v4(), + name: 'Channel tag1', + }, + { + id: v4(), + name: 'Channel tag2', + }, + ], + }) + tags: Tag[]; + + @IsOptional() + @Type(() => Relationship) + @JSONSchema({ + description: "The channel's Tags.", + example: { + notifications: { + count: 13, + }, + members: { + count: 2, + }, + groups: { + count: 1, + }, + }, + }) + relationships: Relationship; + + @IsEnum(ChannelFlags, { each: true }) + @JSONSchema({ + description: 'List of labels related to the channel.', + example: [ChannelFlags.critical, ChannelFlags.mandatory], + }) + channelFlags: ChannelFlags[]; + + constructor(channel: Channel) { + this.id = channel.id; + this.slug = channel.slug; + this.owner = channel.owner; + this.name = channel.name; + this.description = channel.description; + this.members = channel.members; + this.adminGroup = channel.adminGroup; + this.unsubscribed = channel.unsubscribed; + this.groups = channel.groups; + //this.notifications = channel.notifications; + this.visibility = channel.visibility; + this.archive = channel.archive; + this.APIKey = channel.APIKey; + this.creationDate = channel.creationDate; + this.lastActivityDate = channel.lastActivityDate; + this.incomingEmail = channel.incomingEmail; + this.incomingEgroup = channel.incomingEgroup; + this.submissionByEmail = channel.submissionByEmail; + this.submissionByForm = channel.submissionByForm; + this.deleteDate = channel.deleteDate; + this.category = channel.category; + this.tags = channel.tags; + this.sendPrivate = channel.sendPrivate; + this.relationships = new Relationship( + new NotificationsListResponse(channel.notifications), + new MembersListResponse(channel.members), + new GroupsResponse(channel.groups?.length || 0), + ); + this.channelFlags = channel.channelFlags; + } + + async channelFilters(authBag: AuthorizationBag, chan: Channel): Promise<void> { + //this.subscribed = await chan.isSubscribedByName(authBag.userName); + this.manage = await chan.isAdmin(authBag); + this.access = await chan.hasAccess(authBag); + this.send = await chan.canSendByForm(authBag); + } +} + +@JSONSchema({ + description: 'Channel member json object.', + example: {}, +}) +export class MemberResponse { + @IsString() + @IsUUID('4') + @IsNotEmpty() + @IsString() + @JSONSchema({ + description: 'Member id.', + example: v4(), + }) + id: string; + + @IsNotEmpty() + @IsString() + @JSONSchema({ + description: 'Member username.', + example: 'username', + }) + username: string; + + @IsNotEmpty() + @IsString() + @IsEmail() + @JSONSchema({ + description: 'Member email.', + example: 'useremail@cern.ch', + }) + email: string; + + constructor(user: User) { + this.id = user.id; + this.username = user.username; + this.email = user.email; + } +} + +export class GroupResponse { + @IsNotEmpty() + @IsString() + id: string; + + @IsNotEmpty() + @IsString() + groupIdentifier: string; + + constructor(group: Group) { + this.id = group.id; + this.groupIdentifier = group.groupIdentifier; + } +} + +//REQUESTS +export class GroupRequest { + @IsString() + groupIdentifier: string; +} + +@JSONSchema({ + description: 'Channel create request object.', + example: { + name: 'Channel name', + slug: 'channel-name', + description: 'This channel is not real.', + visibility: Visibility.restricted, + archive: true, + submissionByForm: [SubmissionByForm.administrators], + submissionByEmail: [SubmissionByEmail.administrators], + }, +}) +export class CreateChannelRequest { + @IsString() + @JSONSchema({ + description: 'Channel name.', + example: 'Channel name', + }) + name: string; + + @IsString() + @JSONSchema({ + description: 'Channel slug. Usually a slugified version of the name, eg channel-name.', + example: 'channel-name', + }) + slug: string; + + @IsString() + @JSONSchema({ + description: 'Channel description.', + example: 'This channel is not real.', + }) + description: string; + + @IsOptional() + @IsString() + @JSONSchema({ + description: 'Channel description.', + example: 'groupname', + }) + adminGroup: string; + + @IsEnum(Visibility) + @JSONSchema({ + description: "Channel's visibility.", + example: Visibility.restricted, + }) + visibility: Visibility; + + @IsOptional() + @IsString() + @JSONSchema({ + description: + "The channel's incoming egroup for notifications. Needs " + + SubmissionByEmail.egroup + + " to be set on 'submissionByEmail'.", + example: 'egroupname', + }) + incomingEgroup: string; + + @IsOptional() + @IsEmail() + @JSONSchema({ + description: "The channel's incoming email for notifications.", + example: 'channelname+level@cern.ch', + }) + incomingEmail: string; + + @IsBoolean() + @JSONSchema({ + description: + "Allows enabling preservation of the channel's notifications into an external archive. Notifications are deleted from our system (not the archive) after 13 months in any case.", + example: true, + }) + archive: boolean; + + @IsEnum(SubmissionByForm, { each: true }) + @JSONSchema({ + description: 'Who can send notifications via Web or API. Default is ADMINISTRATORS.', + example: [SubmissionByForm.administrators, SubmissionByForm.members, SubmissionByForm.apikey], + }) + submissionByForm: SubmissionByForm[]; + + @IsEnum(SubmissionByEmail, { each: true }) + @JSONSchema({ + description: + 'Who and how can send notifications by email. To use ' + + SubmissionByEmail.egroup + + " option, needs 'incomingEgroup' to be set.", + example: [ + SubmissionByEmail.administrators, + SubmissionByEmail.members, + SubmissionByEmail.email, + SubmissionByEmail.egroup, + ], + }) + submissionByEmail: SubmissionByEmail[]; +} + +@JSONSchema({ + description: 'Information that the channel is to be updated with.', + example: { + id: v4(), + description: 'New channel description.', + name: 'New Channel name', + visibility: Visibility.public, + sendPrivate: false, + archive: true, + submissionByEmail: [], + submissionByForm: [SubmissionByForm.administrators], + }, +}) +export class UpdateChannelRequest { + @IsBoolean() + @JSONSchema({ + description: + "Allows enabling preservation of the channel's notifications into an external archive. Notifications are deleted from our system (not the archive) after 13 months in any case.", + example: true, + }) + archive: boolean; + + @IsString() + @JSONSchema({ + description: 'Channel description.', + example: 'Service updates for CERN Notifications.', + }) + description: string; + + @IsNotEmpty() + @IsUUID('4') + @JSONSchema({ + description: 'Channel id.', + example: v4(), + }) + id: string; + + @IsOptional() + @IsString() + @JSONSchema({ + description: + "The channel's incoming egroup for notifications. Needs " + + SubmissionByEmail.egroup + + " to be set on 'submissionByEmail'.", + example: 'egroupname', + }) + incomingEgroup: string; + + @IsOptional() + @IsEmail() + @JSONSchema({ + description: "The channel's incoming email for notifications.", + example: 'channelname+level@cern.ch', + }) + incomingEmail: string; + + @IsString() + @JSONSchema({ + description: "The channel's name.", + example: 'Channel name', + }) + name: string; + + @IsBoolean() + @JSONSchema({ + description: "The channel's setting for allowing private notifications.", + example: false, + }) + sendPrivate: boolean; + + @IsEnum(SubmissionByEmail, { each: true }) + @JSONSchema({ + description: + 'Determines who and how can send notifications by email. To use ' + + SubmissionByEmail.egroup + + " option, needs 'incomingEgroup' to be set.", + example: [SubmissionByEmail.administrators], + }) + submissionByEmail: SubmissionByEmail[]; + + @IsEnum(SubmissionByForm, { each: true }) + @JSONSchema({ + description: 'Who can send notifications via Web or API. Default is ADMINISTRATORS.', + example: [SubmissionByForm.administrators], + }) + submissionByForm: SubmissionByForm[]; + + @IsEnum(Visibility) + @JSONSchema({ + description: "The channel's visibility.", + example: Visibility.restricted, + }) + visibility: Visibility; +} + +@JSONSchema({ + description: 'List of Tag ids.', + example: { + tagIds: [v4(), v4()], + }, +}) +export class setTagsRequest { + @JSONSchema({ + description: 'List of Tag ids.', + example: [v4(), v4()], + }) + @IsUUID('4', { each: true }) + tagIds: string[]; +} diff --git a/src/controllers/notifications/dto.ts b/src/controllers/notifications/dto.ts index c0a90cab4f96520d14acc8d25765c242dd40aca0..1c74641dd1677dd3fb3e0103c90cdcd9958e15e4 100644 --- a/src/controllers/notifications/dto.ts +++ b/src/controllers/notifications/dto.ts @@ -11,9 +11,9 @@ import { ValidateNested, } from 'class-validator'; import { Type } from 'class-transformer'; +import { JSONSchema } from 'class-validator-jsonschema'; import { Notification } from '../../models/notification'; import { PriorityLevel, Source } from '../../models/notification-enums'; -import { JSONSchema } from 'class-validator-jsonschema'; import { v4 } from 'uuid'; const swaggerChannelId = process.env.SWAGGER_CHANNEL_ID; diff --git a/src/models/cern-authorization-service.ts b/src/models/cern-authorization-service.ts index 099af70c88735056e08651540a11e521dedff80e..91c5f67bf12fd25d3a43fb4cd8b9023893604088 100644 --- a/src/models/cern-authorization-service.ts +++ b/src/models/cern-authorization-service.ts @@ -71,8 +71,7 @@ export class CernAuthorizationService { console.error(ex.error); } } - - return; + return undefined; //search was unsuccesful } static async getGroupIdNoCache(groupIdentifier: string) { diff --git a/src/models/channel.ts b/src/models/channel.ts index 23ea87b2948b75e52ce1ec1702919d315d596621..ce11786f0615bf1026e0a5e11ca4ca2406f7070f 100644 --- a/src/models/channel.ts +++ b/src/models/channel.ts @@ -21,7 +21,7 @@ import { AlphaNumericLowercase, AlphaNumericPunctuationChannelName, FromEmailSet import { AuthorizationBag } from './authorization-bag'; import { SubmissionByEmail, SubmissionByForm, SubscriptionPolicy, Visibility, ChannelFlags } from './channel-enums'; import { ChannelHelpers } from './channel-helpers'; - +import { Type } from 'class-transformer'; import { ApiKeyObject } from './api-key-object'; import { APIKeyTypeEnum } from '../middleware/api-key'; @@ -52,6 +52,7 @@ export class Channel extends ApiKeyObject { }) // @JoinTable() @JoinTable({ name: 'channels_members__users' }) + @Type(() => User) members: User[]; @IsOptional() @@ -61,28 +62,29 @@ export class Channel extends ApiKeyObject { @ManyToMany(type => User, user => user.unsubscribed, { cascade: true, }) - // @JoinTable() @JoinTable({ name: 'channels_unsubscribed__users' }) + @Type(() => User) unsubscribed: User[]; @ManyToMany(type => Group) - // @JoinTable() @JoinTable({ name: 'channels_groups__groups' }) + @Type(() => Group) groups: Group[]; @OneToMany(type => Notification, notification => notification.target, { cascade: true, }) + @Type(() => Notification) notifications: Notification[]; @Column({ enum: Visibility, default: Visibility.restricted }) - visibility: string; + visibility: Visibility; @Column({ enum: SubscriptionPolicy, default: SubscriptionPolicy.selfSubscription, }) - subscriptionPolicy: string; + subscriptionPolicy: SubscriptionPolicy; @Column({ default: true }) archive: boolean; @@ -122,6 +124,7 @@ export class Channel extends ApiKeyObject { nullable: false, default: [], }) + //@Type(() => enum) submissionByEmail: SubmissionByEmail[]; @Column({ @@ -131,6 +134,7 @@ export class Channel extends ApiKeyObject { nullable: false, default: [SubmissionByForm.administrators], }) + //@Type(() => SubmissionByForm) submissionByForm: SubmissionByForm[]; @DeleteDateColumn() @@ -162,6 +166,7 @@ export class Channel extends ApiKeyObject { constructor(channel) { super(); if (channel) { + this.id = channel.id; this.slug = channel.slug; this.name = channel.name; this.description = channel.description; diff --git a/src/services/channels-service.ts b/src/services/channels-service.ts index 27ecc6eacf9cb74deb167a876097ce3a7bfd21bb..f98d50f2581ea1dcfb3e2c501dc267a3755607d0 100644 --- a/src/services/channels-service.ts +++ b/src/services/channels-service.ts @@ -1,101 +1,73 @@ -import { User } from '../models/user'; -import { Channel } from '../models/channel'; -import { Group } from '../models/group'; import { Category } from '../models/category'; -import { Tag } from '../models/tag'; import { AuthorizationBag } from '../models/authorization-bag'; import { ApiKeyService } from './api-key-service'; +import { + ChannelResponse, + ChannelsListResponse, + CreateChannelRequest, + UpdateChannelRequest, + ChannelsQuery, + Query, + MembersListResponse, + GroupsListResponse, + GroupResponse, + GetChannelResponse, + EditChannelResponse, + setTagsRequest, + ChannelStatsResponse, + MemberResponse, +} from '../controllers/channels/dto'; export interface ChannelsService extends ApiKeyService { - getAllChannels(query, authorizationBag: AuthorizationBag): Promise<Channel[]>; + getAllChannels(query: ChannelsQuery, authorizationBag: AuthorizationBag): Promise<ChannelsListResponse>; - getAllPublicChannels(query): Promise<Channel[]>; + getAllPublicChannels(query): Promise<ChannelsListResponse>; //public controller - getChannelById( - channelId: string, - authorizationBag: AuthorizationBag, - ): Promise<Channel>; + getChannelById(channelId: string, authorizationBag: AuthorizationBag): Promise<GetChannelResponse>; - editChannelById( - channelId: string, - authorizationBag: AuthorizationBag, - ): Promise<Channel>; + editChannelById(channelId: string, authorizationBag: AuthorizationBag): Promise<EditChannelResponse>; - createChannel( - channel: Channel, - authorizationBag: AuthorizationBag, - ): Promise<Channel>; + createChannel(channel: CreateChannelRequest, authorizationBag: AuthorizationBag): Promise<ChannelResponse>; - deleteChannel( - channelId: string, - authorizationBag: AuthorizationBag, - ): Promise<Channel>; + deleteChannel(channelId: string, authorizationBag: AuthorizationBag): Promise<string>; - updateChannel( - channel: Channel, - authorizationBag: AuthorizationBag, - ): Promise<Channel>; + updateChannel(channel: UpdateChannelRequest, authorizationBag: AuthorizationBag): Promise<ChannelResponse>; - getChannelMembers( - channelId: String, - query, - authorizationBag: AuthorizationBag, - ): Promise<User[]>; + updateChannel(channel: UpdateChannelRequest, authorizationBag: AuthorizationBag): Promise<ChannelResponse>; - getChannelGroups( - channelId: String, - query, - authorizationBag: AuthorizationBag, - ): Promise<Group[]>; + getChannelMembers(channelId: string, query: Query, authorizationBag: AuthorizationBag): Promise<MembersListResponse>; - addGroupToChannel( - group: Group, - channelId: string, - authorizationBag: AuthorizationBag, - ): Promise<Channel>; + getChannelGroups(channelId: string, query: Query, authorizationBag: AuthorizationBag): Promise<GroupsListResponse>; - removeGroupFromChannel(groupId: string, channalId: string, authorizationBag: AuthorizationBag): Promise<Channel>; + addGroupToChannel(groupName: string, channelId: string, authorizationBag: AuthorizationBag): Promise<GroupResponse>; + + removeGroupFromChannel(groupId: string, channelId: string, authorizationBag: AuthorizationBag): Promise<string>; updateChannelAdminGroup( - group: Group, + groupName: string, channelId: string, authorizationBag: AuthorizationBag, - ): Promise<Channel>; + ): Promise<ChannelResponse>; addMemberToChannel( membername: string, channelId: string, authorizationBag: AuthorizationBag, - ): Promise<Channel>; + ): Promise<MemberResponse>; - removeMemberFromChannel(memberId: string, channelId: string, authorizationBag: AuthorizationBag): Promise<Channel>; + removeMemberFromChannel(memberId: string, channelId: string, authorizationBag: AuthorizationBag): Promise<string>; - subscribeToChannel( - channelId: string, - authorizationBag: AuthorizationBag, - ): Promise<Channel>; + subscribeToChannel(channelId: string, authorizationBag: AuthorizationBag): Promise<ChannelResponse>; - unsubscribeFromChannel( - channelId: string, - authorizationBag: AuthorizationBag, - ): Promise<Channel>; + unsubscribeFromChannel(channelId: string, authorizationBag: AuthorizationBag): Promise<ChannelResponse>; - setCategory(channelId: string, category: Category, authorizationBag: AuthorizationBag): Promise<Channel>; + generateApiKey(channelId: string, authorizationBag: AuthorizationBag): Promise<string>; - setTags( - channelId: string, - tags: Tag[], - authorizationBag: AuthorizationBag, - ): Promise<Channel>; + setCategory(channelId: string, category: Category, authorizationBag: AuthorizationBag): Promise<ChannelResponse>; - setChannelOwner( - username: string, - channelId: string, - authorizationBag: AuthorizationBag, - ): Promise<Channel>; + setTags(channelId: string, tags: setTagsRequest, authorizationBag: AuthorizationBag): Promise<ChannelResponse>; - getChannelStats( - channelId: string, - authorizationBag: AuthorizationBag, - ): Promise<Channel>; + setChannelOwner(username: string, channelId: string, authorizationBag: AuthorizationBag): Promise<ChannelResponse>; + + getChannelStats(channelId: string, authorizationBag: AuthorizationBag): Promise<ChannelStatsResponse>; } diff --git a/src/services/impl/channels-service-impl.ts b/src/services/impl/channels-service-impl.ts index 62347f84f9a3029a13423a1ce14db22095dd0d48..557dd6d6bec8cd157f23edba490765cf750c58e9 100644 --- a/src/services/impl/channels-service-impl.ts +++ b/src/services/impl/channels-service-impl.ts @@ -1,3 +1,7 @@ +import { SetChannelOwner } from './channels/set-channel-owner'; +import { ApiKeyService } from '../api-key-service'; +import { VerifyApiKey } from './api-key/verify-api-key'; +import { GetChannelStats } from './channels/get-channel-stats'; import { ChannelsService } from '../channels-service'; import { AbstractService } from './abstract-service'; import { Channel } from '../../models/channel'; @@ -6,12 +10,10 @@ import { FindChannelById } from './channels/find-channel-by-id'; import { EditChannelById } from './channels/edit-channel-by-id'; import { CreateChannel } from './channels/create-channel'; import { AddGroupToChannel } from './channels/add-group-to-channel'; -import { Group } from '../../models/group'; import { SubscribeToChannel } from './channels/subscribe-to-channel'; import { UnsubscribeFromChannel } from './channels/unsubscribe-from-channel'; import { UpdateChannel } from './channels/update-channel'; import { GetChannelMembers } from './channels/get-channel-members'; -import { User } from '../../models/user'; import { GetChannelGroups } from './channels/get-channel-groups'; import { FindPublicChannels } from './channels/find-public-channels'; import { AddMemberToChannel } from './channels/add-member-to-channel'; @@ -22,94 +24,111 @@ import { UpdateChannelAdminGroup } from './channels/update-channel-admin-group'; import { GenerateApiKey } from './channels/generate-api-key'; import { AuthorizationBag } from '../../models/authorization-bag'; import { Category } from '../../models/category'; -import { Tag } from '../../models/tag'; import { SetCategory } from './channels/set-category'; import { SetTags } from './channels/set-tags'; -import { SetChannelOwner } from './channels/set-channel-owner'; -import { ApiKeyService } from '../api-key-service'; -import { VerifyApiKey } from './api-key/verify-api-key'; -import { GetChannelStats } from './channels/get-channel-stats'; - +import { + ChannelResponse, + ChannelsListResponse, + CreateChannelRequest, + UpdateChannelRequest, + MembersListResponse, + GroupsListResponse, + ChannelsQuery, + GroupResponse, + GetChannelResponse, + EditChannelResponse, + setTagsRequest, + ChannelStatsResponse, + MemberResponse, + Query, +} from '../../controllers/channels/dto'; export class ChannelsServiceImpl extends AbstractService implements ChannelsService, ApiKeyService { - getAllChannels(query, authorizationBag: AuthorizationBag): Promise<Channel[]> { + getAllChannels(query: ChannelsQuery, authorizationBag: AuthorizationBag): Promise<ChannelsListResponse> { return this.commandExecutor.execute(new FindAllChannels(query, authorizationBag)); } - getAllPublicChannels(query): Promise<Channel[]> { + getAllPublicChannels(query: Query): Promise<ChannelsListResponse> { return this.commandExecutor.execute(new FindPublicChannels(query)); } - getChannelById(channelId: string, authorizationBag: AuthorizationBag): Promise<Channel> { + getChannelById(channelId: string, authorizationBag: AuthorizationBag): Promise<GetChannelResponse> { return this.commandExecutor.execute(new FindChannelById(channelId, authorizationBag)); } - - editChannelById(channelId: string, authorizationBag: AuthorizationBag): Promise<Channel> { + editChannelById(channelId: string, authorizationBag: AuthorizationBag): Promise<EditChannelResponse> { return this.commandExecutor.execute(new EditChannelById(channelId, authorizationBag)); } - createChannel(channel: Channel, authorizationBag: AuthorizationBag): Promise<Channel> { + createChannel(channel: CreateChannelRequest, authorizationBag: AuthorizationBag): Promise<ChannelResponse> { return this.commandExecutor.execute(new CreateChannel(channel, authorizationBag)); } - deleteChannel(channelId: string, authorizationBag: AuthorizationBag): Promise<Channel> { + deleteChannel(channelId: string, authorizationBag: AuthorizationBag): Promise<string> { return this.commandExecutor.execute(new DeleteChannel(channelId, authorizationBag)); } - updateChannel(channel: Channel, authorizationBag: AuthorizationBag): Promise<Channel> { + updateChannel(channel: UpdateChannelRequest, authorizationBag: AuthorizationBag): Promise<ChannelResponse> { return this.commandExecutor.execute(new UpdateChannel(channel, authorizationBag)); } - getChannelMembers(channelId: string, query, authorizationBag: AuthorizationBag): Promise<User[]> { + getChannelMembers(channelId: string, query: Query, authorizationBag: AuthorizationBag): Promise<MembersListResponse> { return this.commandExecutor.execute(new GetChannelMembers(channelId, query, authorizationBag)); } - getChannelGroups(channelId: string, query, authorizationBag: AuthorizationBag): Promise<Group[]> { + getChannelGroups(channelId: string, query: Query, authorizationBag: AuthorizationBag): Promise<GroupsListResponse> { return this.commandExecutor.execute(new GetChannelGroups(channelId, query, authorizationBag)); } - addGroupToChannel(group: Group, channelId: string, authorizationBag: AuthorizationBag): Promise<Channel> { - return this.commandExecutor.execute(new AddGroupToChannel(group, channelId, authorizationBag)); + addGroupToChannel(groupName: string, channelId: string, authorizationBag: AuthorizationBag): Promise<GroupResponse> { + return this.commandExecutor.execute(new AddGroupToChannel(groupName, channelId, authorizationBag)); } - removeGroupFromChannel(groupId: string, channelId: string, authorizationBag: AuthorizationBag): Promise<Channel> { + removeGroupFromChannel(groupId: string, channelId: string, authorizationBag: AuthorizationBag): Promise<string> { return this.commandExecutor.execute(new RemoveGroupFromChannel(groupId, channelId, authorizationBag)); } - updateChannelAdminGroup(group: Group, channelId: string, authorizationBag: AuthorizationBag): Promise<Channel> { - return this.commandExecutor.execute(new UpdateChannelAdminGroup(group, channelId, authorizationBag)); + updateChannelAdminGroup( + groupName: string, + channelId: string, + authorizationBag: AuthorizationBag, + ): Promise<ChannelResponse> { + return this.commandExecutor.execute(new UpdateChannelAdminGroup(groupName, channelId, authorizationBag)); } - addMemberToChannel(membername: string, channelId: string, authorizationBag: AuthorizationBag): Promise<Channel> { + addMemberToChannel( + membername: string, + channelId: string, + authorizationBag: AuthorizationBag, + ): Promise<MemberResponse> { return this.commandExecutor.execute(new AddMemberToChannel(membername, channelId, authorizationBag)); } - removeMemberFromChannel(memberId: string, channelId: string, authorizationBag: AuthorizationBag): Promise<Channel> { + removeMemberFromChannel(memberId: string, channelId: string, authorizationBag: AuthorizationBag): Promise<string> { return this.commandExecutor.execute( // missing username to check permission new RemoveUserFromChannel(memberId, channelId, authorizationBag), ); } - subscribeToChannel(channelId: string, authorizationBag: AuthorizationBag): Promise<Channel> { + subscribeToChannel(channelId: string, authorizationBag: AuthorizationBag): Promise<ChannelResponse> { return this.commandExecutor.execute(new SubscribeToChannel(authorizationBag.userId, channelId, authorizationBag)); } - unsubscribeFromChannel(channelId: string, authorizationBag: AuthorizationBag): Promise<Channel> { + unsubscribeFromChannel(channelId: string, authorizationBag: AuthorizationBag): Promise<ChannelResponse> { return this.commandExecutor.execute( new UnsubscribeFromChannel(authorizationBag.userId, channelId, authorizationBag), ); } - setCategory(channelId: string, category: Category, authorizationBag: AuthorizationBag): Promise<Channel> { + setCategory(channelId: string, category: Category, authorizationBag: AuthorizationBag): Promise<ChannelResponse> { return this.commandExecutor.execute(new SetCategory(channelId, category, authorizationBag)); } - setTags(channelId: string, tags: Tag[], authorizationBag: AuthorizationBag): Promise<Channel> { + setTags(channelId: string, tags: setTagsRequest, authorizationBag: AuthorizationBag): Promise<ChannelResponse> { return this.commandExecutor.execute(new SetTags(channelId, tags, authorizationBag)); } - setChannelOwner(username: string, channelId: string, authorizationBag: AuthorizationBag): Promise<Channel> { + setChannelOwner(username: string, channelId: string, authorizationBag: AuthorizationBag): Promise<ChannelResponse> { return this.commandExecutor.execute(new SetChannelOwner(username, channelId, authorizationBag)); } @@ -121,12 +140,7 @@ export class ChannelsServiceImpl extends AbstractService implements ChannelsServ return this.commandExecutor.execute(new VerifyApiKey(id, key, Channel)); } - getChannelStats( - channelId: string, - authorizationBag: AuthorizationBag, - ): Promise<Channel> { - return this.commandExecutor.execute( - new GetChannelStats(channelId, authorizationBag), - ); + getChannelStats(channelId: string, authorizationBag: AuthorizationBag): Promise<ChannelStatsResponse> { + return this.commandExecutor.execute(new GetChannelStats(channelId, authorizationBag)); } -} \ No newline at end of file +} diff --git a/src/services/impl/channels/add-group-to-channel.ts b/src/services/impl/channels/add-group-to-channel.ts index be9c4df04d2bb9d77673e1746e3d634850338327..e850a794de0f0fbcdf7a9029e27e53dddbd83a6f 100644 --- a/src/services/impl/channels/add-group-to-channel.ts +++ b/src/services/impl/channels/add-group-to-channel.ts @@ -1,19 +1,22 @@ import { Command } from '../command'; import { EntityManager } from 'typeorm'; import { Channel } from '../../../models/channel'; -import { Group } from '../../../models/group'; import { ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; import { ServiceFactory } from '../../services-factory'; import { GroupsServiceInterface } from '../../groups-service'; import { AuditChannels } from '../../../log/auditing'; +import { ChannelResponse } from '../../../controllers/channels/dto'; +import { Group } from "../../../models/group"; +import { GroupResponse } from "../../../controllers/channels/dto"; +import { CernAuthorizationService } from "../../../models/cern-authorization-service"; export class AddGroupToChannel implements Command { - constructor(private group: Group, private channelId: string, private authorizationBag: AuthorizationBag) {} - + constructor(private groupName: string, private channelId: string, private authorizationBag: AuthorizationBag) { } groupsService: GroupsServiceInterface = ServiceFactory.getGroupService(); + - async execute(transactionManager: EntityManager): Promise<Channel> { + async execute(transactionManager: EntityManager): Promise<GroupResponse> { const channel = await transactionManager.findOne(Channel, { relations: ['owner', 'adminGroup', 'groups'], where: { @@ -28,19 +31,19 @@ export class AddGroupToChannel implements Command { throw new ForbiddenError("You don't have the rights to manage this channel."); //if added group was already added - if (channel.groups.filter(g => g.groupIdentifier === this.group.groupIdentifier).length > 0) { - throw new ForbiddenError(`Group ${this.group.groupIdentifier} is already a member.`); + if (channel.groups.filter(g => g.groupIdentifier === this.groupName).length > 0) { + throw new ForbiddenError(`Group ${this.groupName} is already a member.`); } - const groupToAdd = await this.groupsService.GetOrCreateGroup(this.group.groupIdentifier); + const groupToAdd = await this.groupsService.GetOrCreateGroup(this.groupName); channel.addGroup(groupToAdd); const updatedChannel = await transactionManager.save(channel); await AuditChannels.setValue(updatedChannel.id, { event: 'AddGroup', user: this.authorizationBag.email, - groupIdentifier: this.group.groupIdentifier, + groupIdentifier: this.groupName, }); - return updatedChannel; + return new GroupResponse(groupToAdd); } } diff --git a/src/services/impl/channels/add-member-to-channel.ts b/src/services/impl/channels/add-member-to-channel.ts index bf2d1336d8c46e09d1a924f876b220416b661907..a71b37b0cc42d7ba8a48459600ec6a5239ca1d74 100644 --- a/src/services/impl/channels/add-member-to-channel.ts +++ b/src/services/impl/channels/add-member-to-channel.ts @@ -7,6 +7,9 @@ import { ServiceFactory } from '../../services-factory'; import { AuthorizationBag } from '../../../models/authorization-bag'; import { UsersServiceInterface } from '../../users-service'; import { AuditChannels } from '../../../log/auditing'; +import { AuthService } from "../../../services/auth-service"; +import { CernAuthorizationService } from "../../../models/cern-authorization-service"; +import { MemberResponse, ChannelResponse } from "../../../controllers/channels/dto"; export class AddMemberToChannel implements Command { constructor(private membername: string, private channelId: string, private authorizationBag: AuthorizationBag) {} @@ -19,7 +22,7 @@ export class AddMemberToChannel implements Command { return new User({ username: this.membername }); } - async execute(transactionManager: EntityManager): Promise<Channel> { + async execute(transactionManager: EntityManager): Promise<MemberResponse> { const channel = await transactionManager.findOne(Channel, { relations: ['owner', 'adminGroup', 'members', 'groups'], where: { @@ -55,6 +58,6 @@ export class AddMemberToChannel implements Command { membername: this.membername, }); - return channel; + return new MemberResponse(userToAdd); } } diff --git a/src/services/impl/channels/create-channel.ts b/src/services/impl/channels/create-channel.ts index b47ccf1ada3e371fa78c90323547364042b8b772..16d492b5da5f7ec78325fe195a34293eaa4fda1a 100644 --- a/src/services/impl/channels/create-channel.ts +++ b/src/services/impl/channels/create-channel.ts @@ -8,11 +8,13 @@ import { User } from '../../../models/user'; import { AuthorizationBag } from '../../../models/authorization-bag'; import { prepareValidationErrorList } from './validation-utils'; import { AuditChannels } from '../../../log/auditing'; +import { ChannelResponse, CreateChannelRequest } from '../../../controllers/channels/dto'; +import { SubscriptionPolicy } from '../../../models/channel-enums'; export class CreateChannel implements Command { - constructor(private channel: Channel, private authorizationBag: AuthorizationBag) {} + constructor(private channel: CreateChannelRequest, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager): Promise<Channel> { + async execute(transactionManager: EntityManager): Promise<ChannelResponse> { // Get owner (user who have created the channel) to set as user by default const user = await transactionManager.findOne(User, { id: this.authorizationBag.userId, @@ -20,9 +22,9 @@ export class CreateChannel implements Command { const channel = new Channel({ ...this.channel, - adminGroup: this.channel.adminGroup.groupIdentifier !== '' ? new Group(this.channel.adminGroup) : undefined, owner: this.authorizationBag.userId, members: [user], + subscriptionPolicy: SubscriptionPolicy.dynamic, //TODO TO be removed once feature is added }); channel.name = channel.name.trim(); @@ -48,8 +50,12 @@ export class CreateChannel implements Command { throw new BadRequestError('Channel slug already exists'); } - if (channel.adminGroup && !(await channel.adminGroup.exists())) { - throw new NotFoundError('Admin group does not exist'); + if (this.channel.adminGroup) { + const adminGroup = await transactionManager.findOne(Group, { groupIdentifier: this.channel.adminGroup }); + if (!adminGroup) throw new NotFoundError('Admin group does not exist'); + if (adminGroup.groupIdentifier != this.channel.adminGroup) + throw new Error('Admin group name does not match provided id.'); + channel.adminGroup = adminGroup; } const validationErrors = await validate(channel); @@ -60,6 +66,7 @@ export class CreateChannel implements Command { await AuditChannels.setValue(createdChannel.id, { event: 'Create', user: this.authorizationBag.email }); - return createdChannel; + //TODO could go wrong if transaction manager fails. test this + return new ChannelResponse(createdChannel); } } diff --git a/src/services/impl/channels/delete-channel.ts b/src/services/impl/channels/delete-channel.ts index b02335c8a3814faa264706df31119217777066c3..057cd928f1d9da022ae43f7d457583f94b927a56 100644 --- a/src/services/impl/channels/delete-channel.ts +++ b/src/services/impl/channels/delete-channel.ts @@ -1,5 +1,5 @@ import { Command } from '../command'; -import { EntityManager } from 'typeorm'; +import { EntityManager, UpdateResult } from 'typeorm'; import { Channel } from '../../../models/channel'; import { ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; @@ -8,7 +8,7 @@ import { AuditChannels } from '../../../log/auditing'; export class DeleteChannel implements Command { constructor(private channelId: string, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager): Promise<any> { + async execute(transactionManager: EntityManager): Promise<string> { const channel = await transactionManager.findOne(Channel, { relations: ['members', 'groups', 'owner', 'adminGroup'], where: { @@ -27,9 +27,12 @@ export class DeleteChannel implements Command { channel.slug = `${channel.slug}-DELETED-${Date.now()}`; await transactionManager.save(channel); - const deletedChannel = await transactionManager.softDelete(Channel, channel.id); - await AuditChannels.setValue(channel.id, { event: 'Delete', user: this.authorizationBag.email }); - - return deletedChannel; + let result = await transactionManager.softDelete(Channel, channel.id); + if (result.affected === 1) { + await AuditChannels.setValue(channel.id, { event: 'Delete', user: this.authorizationBag.email }); + return channel.id; + } + else + throw new ForbiddenError("The channel does not exist or you are not allowed to delete it."); } } diff --git a/src/services/impl/channels/edit-channel-by-id.ts b/src/services/impl/channels/edit-channel-by-id.ts index e422ac4d031ea0e48d65af49857561d1c51aaded..df99635746f233044fba784ea98ffbd93431bd31 100644 --- a/src/services/impl/channels/edit-channel-by-id.ts +++ b/src/services/impl/channels/edit-channel-by-id.ts @@ -1,32 +1,26 @@ -import { Command } from "../command"; -import { EntityManager } from "typeorm"; -import { Channel } from "../../../models/channel"; -import { ForbiddenError, NotFoundError } from "routing-controllers"; -import { AuthorizationBag } from "../../../models/authorization-bag"; +import { Command } from '../command'; +import { EntityManager } from 'typeorm'; +import { Channel } from '../../../models/channel'; +import { ForbiddenError, NotFoundError } from 'routing-controllers'; +import { AuthorizationBag } from '../../../models/authorization-bag'; +import { EditChannelResponse } from '../../../controllers/channels/dto'; export class EditChannelById implements Command { - constructor(private channelId: string, private authorizationBag: AuthorizationBag) { } + constructor(private channelId: string, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager): Promise<any> { - let channel = await transactionManager.findOne(Channel, { - //relations: ["adminGroup"] + async execute(transactionManager: EntityManager): Promise<EditChannelResponse> { + const channel = await transactionManager.findOne(Channel, { // Specify needed joinColumns targets here, or use eager=true in model column def. - relations: ["members", "groups", "owner", "adminGroup", "category", "tags"], + relations: ['members', 'groups', 'owner', 'adminGroup', 'category', 'tags'], //needs owner and more otherwise isAdmin wont work where: { id: this.channelId, }, }); - if (!channel) - throw new NotFoundError("Channel does not exist"); + if (!channel) throw new NotFoundError('Channel does not exist'); - if (!(await channel.isAdmin(this.authorizationBag))) - throw new ForbiddenError("Access to Channel not Authorized !"); + if (!(await channel.isAdmin(this.authorizationBag))) throw new ForbiddenError('Access to Channel not Authorized !'); - return { - ...channel, - manage: true, - send: await channel.canSendByForm(this.authorizationBag), - }; + return new EditChannelResponse(channel); } } diff --git a/src/services/impl/channels/find-all-channels.ts b/src/services/impl/channels/find-all-channels.ts index 61ff413f4c05c0d44798760665897dd9b2ec4e58..b7e2056358e938277ba4b440f859c67c25662896 100644 --- a/src/services/impl/channels/find-all-channels.ts +++ b/src/services/impl/channels/find-all-channels.ts @@ -5,14 +5,14 @@ import { AuthorizationBag } from '../../../models/authorization-bag'; import { CernAuthorizationService } from '../../../models/cern-authorization-service'; import { UserChannelCollection, UserChannelCollectionType } from '../../../models/user-channel-collection'; import { UnauthorizedError } from 'routing-controllers'; +import { ChannelsListResponse, ListChannelsFilterObject, ChannelsQuery } from '../../../controllers/channels/dto'; export class FindAllChannels implements Command { - constructor(private query, private authorizationBag: AuthorizationBag) {} + constructor(private query: ChannelsQuery, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager): Promise<any> { - if (!this.authorizationBag) { + async execute(transactionManager: EntityManager): Promise<ChannelsListResponse> { + if (!this.authorizationBag) throw new UnauthorizedError('Access unauthorized. Please authenticate or use /public/channels instead.'); - } console.time('get-all-channels:total:' + this.authorizationBag.userName); @@ -69,7 +69,7 @@ export class FindAllChannels implements Command { // Filter by member, group, admin, etc. qb.andWhere( new Brackets(channelsQB => { - if (this.query.ownerFilter === 'true') { + if (this.query.ownerFilter) { channelsQB.orWhere('owner.id = :userId', { userId: this.authorizationBag.userId, }); @@ -80,7 +80,7 @@ export class FindAllChannels implements Command { return; } - if (this.query.subscribedFilter === 'true') { + if (this.query.subscribedFilter) { channelsQB.orWhere(':userId IN (members.id)', { userId: this.authorizationBag.userId, }); @@ -117,7 +117,7 @@ export class FindAllChannels implements Command { }), ); - if (this.query.subscribedFilter === 'true') { + if (this.query.subscribedFilter) { // exclude all unsubscribed channels qb.andWhere(sqb => { const subQuery = sqb @@ -133,7 +133,7 @@ export class FindAllChannels implements Command { }); } - if (this.query.favoritesFilter === 'true') { + if (this.query.favoritesFilter) { qb.andWhere(sqb => { const subQuery = sqb .subQuery() @@ -172,8 +172,8 @@ export class FindAllChannels implements Command { qb.select(['channel.id', 'channel.name']) // select channel_id .orderBy('channel.name', 'ASC') .distinct() - .limit(parseInt(this.query.skip) + parseInt(this.query.take) || 10) - .offset(parseInt(this.query.skip) || 0); + .limit(this.query.skip + this.query.take || 10) + .offset(this.query.skip || 0); const channel_ids = await qb.getRawMany(); console.timeEnd('get-all-channels:query:' + this.authorizationBag.userName); @@ -200,12 +200,21 @@ export class FindAllChannels implements Command { console.timeEnd('get-all-channels:query-page:' + this.authorizationBag.userName); console.time('get-all-channels:build-return:' + this.authorizationBag.userName); - const returnChannels = await Promise.all( + const returnChannels: ListChannelsFilterObject[] = await Promise.all( channels.map(async channel => { // Clearing privacy items, used in relation above for permissions queries // But should not be returned to the frontend for display // at least don't return channel.owner, channel.adminGroup, channel.members, channel.groups - return { + + return new ListChannelsFilterObject( + channel, + await channel.isSubscribedWithCache(this.authorizationBag.user, userGroups), + await channel.isAdminWithCache(this.authorizationBag, userGroups), + await channel.canSendByFormWithCache(this.authorizationBag, userGroups), + ); + + /* + let filteredChannel = { id: channel.id, slug: channel.slug, name: channel.name, @@ -234,14 +243,12 @@ export class FindAllChannels implements Command { }, sendPrivate: channel.sendPrivate, }; + return ChannelResponse(filteredChannel);*/ }), ); console.timeEnd('get-all-channels:build-return:' + this.authorizationBag.userName); console.timeEnd('get-all-channels:total:' + this.authorizationBag.userName); - return { - channels: returnChannels, - count: count, - }; + return new ChannelsListResponse(returnChannels); } } diff --git a/src/services/impl/channels/find-channel-by-id.ts b/src/services/impl/channels/find-channel-by-id.ts index f6c2bf7b8aa5ce4004568c55733b4fdc19bd328d..892a2e2fad4293a5f84c7aab8623be49db0edfc3 100644 --- a/src/services/impl/channels/find-channel-by-id.ts +++ b/src/services/impl/channels/find-channel-by-id.ts @@ -1,13 +1,14 @@ -import { Command } from '../command'; -import { EntityManager } from 'typeorm'; -import { Channel } from '../../../models/channel'; -import { ForbiddenError, NotFoundError } from 'routing-controllers'; -import { AuthorizationBag } from '../../../models/authorization-bag'; +import { Command } from "../command"; +import { EntityManager } from "typeorm"; +import { Channel } from "../../../models/channel"; +import { ForbiddenError, NotFoundError } from "routing-controllers"; +import { AuthorizationBag } from "../../../models/authorization-bag"; +import {ChannelResponse, GetChannelResponse} from "../../../controllers/channels/dto"; export class FindChannelById implements Command { constructor(private channelId: string, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager): Promise<any> { + async execute(transactionManager: EntityManager): Promise<GetChannelResponse> { let channel = await transactionManager.findOne(Channel, { // Specify needed joinColumns targets here, or use eager=true in model column def. relations: ['members', 'groups', 'owner', 'adminGroup', 'category', 'tags'], @@ -25,7 +26,7 @@ export class FindChannelById implements Command { // Clearing privacy items, used in relation above for permissions queries // But should not be returned to the frontend for display // at least don't return channel.owner, channel.adminGroup, channel.members, channel.groups - return { + let chan = new Channel({ id: channel.id, slug: channel.slug, name: channel.name, @@ -40,6 +41,8 @@ export class FindChannelById implements Command { tags: channel.tags, sendPrivate: channel.sendPrivate, channelFlags: channel.channelFlags, - }; // as Channel; + }); + + return new GetChannelResponse(chan); } } diff --git a/src/services/impl/channels/find-public-channels.ts b/src/services/impl/channels/find-public-channels.ts index e8c11235f6718e715143024803cc3bac4c27bda9..8145554bac6f045b4daaab35ebd3a12f013f22e0 100644 --- a/src/services/impl/channels/find-public-channels.ts +++ b/src/services/impl/channels/find-public-channels.ts @@ -2,11 +2,12 @@ import { Command } from '../command'; import { EntityManager, Like } from 'typeorm'; import { Channel } from '../../../models/channel'; import { Visibility } from '../../../models/channel-enums'; +import { ChannelResponse, ChannelsListResponse } from "../../../controllers/channels/dto"; export class FindPublicChannels implements Command { constructor(private query) {} - async execute(transactionManager: EntityManager): Promise<any> { + async execute(transactionManager: EntityManager): Promise<ChannelsListResponse> { const [channels, count] = await transactionManager.findAndCount(Channel, { relations: ['owner', 'category', 'tags'], where: [ @@ -29,10 +30,6 @@ export class FindPublicChannels implements Command { name: 'ASC', }, }); - - return { - channels, - count, - }; + return new ChannelsListResponse(channels.map(channel => new ChannelResponse(channel))); } } diff --git a/src/services/impl/channels/generate-api-key.ts b/src/services/impl/channels/generate-api-key.ts index 4b74ca3fde039c9f6b9cad0abe8e0513fe373aeb..c1910cfaac46568ddefdfa76e13099e4aab52c8e 100644 --- a/src/services/impl/channels/generate-api-key.ts +++ b/src/services/impl/channels/generate-api-key.ts @@ -4,11 +4,12 @@ import { Channel } from '../../../models/channel'; import { ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; import { AuditChannels } from '../../../log/auditing'; +import { ChannelResponse } from "../../../controllers/channels/dto"; export class GenerateApiKey implements Command { constructor(private channelId: string, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager): Promise<String> { + async execute(transactionManager: EntityManager): Promise<string> { const channel = await transactionManager.findOne(Channel, { relations: ['owner', 'adminGroup'], where: { diff --git a/src/services/impl/channels/get-channel-groups.ts b/src/services/impl/channels/get-channel-groups.ts index a48faa07d043acfe2b2b268adf2625b984495c20..0b4a62b94094ac85a3312fc0eeeb14048e6d3160 100644 --- a/src/services/impl/channels/get-channel-groups.ts +++ b/src/services/impl/channels/get-channel-groups.ts @@ -3,15 +3,12 @@ import { Channel } from '../../../models/channel'; import { EntityManager } from 'typeorm'; import { ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; +import { GroupsListResponse, Query } from '../../../controllers/channels/dto'; export class GetChannelGroups implements Command { - constructor( - private channelId: string, - private query, - private authorizationBag: AuthorizationBag, - ) {} + constructor(private channelId: string, private query: Query, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager): Promise<any> { + async execute(transactionManager: EntityManager): Promise<GroupsListResponse> { const channel = await transactionManager.findOne(Channel, { relations: ['owner', 'adminGroup', 'groups'], where: { @@ -23,20 +20,13 @@ export class GetChannelGroups implements Command { // you need to be channel admin if (!(await channel.isAdmin(this.authorizationBag))) - throw new ForbiddenError( - "You don't have the rights to manage this channel.", - ); + throw new ForbiddenError("You don't have the rights to manage this channel."); - let returnGroups = channel.groups.filter(g => - g.groupIdentifier.includes(this.query.searchText), - ); + const returnGroups = channel.groups.filter(g => g.groupIdentifier.includes(this.query.searchText || '')); return { totalNumberOfGroups: returnGroups.length, - groups: returnGroups.splice( - parseInt(this.query.skip) || 0, - parseInt(this.query.take) || 10, - ), + groups: returnGroups.splice(this.query.skip || 0, this.query.take || 10), }; } } diff --git a/src/services/impl/channels/get-channel-members.ts b/src/services/impl/channels/get-channel-members.ts index bfbeed2f3b37934c63fba0ab9f3577807b5b2f38..06bd14390853e936744a7dfde6acb090b20e8dcc 100644 --- a/src/services/impl/channels/get-channel-members.ts +++ b/src/services/impl/channels/get-channel-members.ts @@ -3,15 +3,12 @@ import { Channel } from '../../../models/channel'; import { EntityManager } from 'typeorm'; import { ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; +import { MembersListResponse, Query } from '../../../controllers/channels/dto'; export class GetChannelMembers implements Command { - constructor( - private channelId: string, - private query, - private authorizationBag: AuthorizationBag, - ) {} + constructor(private channelId: string, private query: Query, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager): Promise<any> { + async execute(transactionManager: EntityManager): Promise<MembersListResponse> { const channel = await transactionManager.findOne(Channel, { relations: ['owner', 'adminGroup', 'members'], where: { @@ -23,22 +20,17 @@ export class GetChannelMembers implements Command { // you need to be channel admin if (!(await channel.isAdmin(this.authorizationBag))) - throw new ForbiddenError( - "You don't have the rights to manage this channel.", - ); + throw new ForbiddenError("You don't have the rights to manage this channel."); - let returnMembers = channel.members.filter( + const returnMembers = channel.members.filter( m => - (m.username && m.username.includes(this.query.searchText)) || - m.email.includes(this.query.searchText), + (m.username && m.username.includes(this.query.searchText || '')) || + m.email.includes(this.query.searchText || ''), ); return { totalNumberOfMembers: returnMembers.length, - members: returnMembers.splice( - parseInt(this.query.skip) || 0, - parseInt(this.query.take) || 10, - ), + members: returnMembers.splice(this.query.skip || 0, this.query.take || 10), }; } } diff --git a/src/services/impl/channels/get-channel-stats.ts b/src/services/impl/channels/get-channel-stats.ts index 900e2e6492c9d9a03ff3241c62a73b35bfc2b412..a145a12e668b7ff5119de7944b4608a102386e74 100644 --- a/src/services/impl/channels/get-channel-stats.ts +++ b/src/services/impl/channels/get-channel-stats.ts @@ -3,15 +3,13 @@ import { EntityManager } from 'typeorm'; import { Channel } from '../../../models/channel'; import { ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; +import { ChannelStatsResponse } from '../../../controllers/channels/dto'; export class GetChannelStats implements Command { - constructor( - private channelId: string, - private authorizationBag: AuthorizationBag, - ) {} + constructor(private channelId: string, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager): Promise<any> { - let channel = await transactionManager.findOne(Channel, { + async execute(transactionManager: EntityManager): Promise<ChannelStatsResponse> { + const channel = await transactionManager.findOne(Channel, { // Specify needed joinColumns targets here, or use eager=true in model column def. relations: ['members', 'groups', 'owner', 'unsubscribed'], where: { @@ -24,15 +22,6 @@ export class GetChannelStats implements Command { if (!(await channel.hasAccess(this.authorizationBag))) throw new ForbiddenError('Access to Channel not Authorized !'); - return { - id: channel.id, - name: channel.name, - members: channel.members.length, - unsubscribed: channel.unsubscribed.length, - owner: channel.owner, - creationDate: channel.creationDate, - lastActivityDate: channel.lastActivityDate, - groups: channel.groups.length, - }; // as Channel; + return new ChannelStatsResponse(channel); } } diff --git a/src/services/impl/channels/remove-group-from-channel.ts b/src/services/impl/channels/remove-group-from-channel.ts index 9149680f19d1b27fdfef5b5c43837e8d2297b886..9866cb78d7e90b13e8de8cd73ae20f9a14f13efa 100644 --- a/src/services/impl/channels/remove-group-from-channel.ts +++ b/src/services/impl/channels/remove-group-from-channel.ts @@ -2,14 +2,20 @@ import { Command } from '../command'; import { EntityManager } from 'typeorm'; import { Channel } from '../../../models/channel'; import { Group } from '../../../models/group'; -import { ForbiddenError, NotFoundError } from 'routing-controllers'; +import { BadRequestError, ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; import { AuditChannels } from '../../../log/auditing'; +import { isUUID } from 'class-validator'; export class RemoveGroupFromChannel implements Command { constructor(private groupId: string, private channelId: string, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager) { + async execute(transactionManager: EntityManager): Promise<string> { + if (!isUUID(this.channelId)) + throw new BadRequestError('Provided channel id: "' + this.channelId + '" is not a UUID.'); + + if (!isUUID(this.groupId)) throw new BadRequestError('Provided group id: "' + this.groupId + '" is not a UUID.'); + const channel = await transactionManager.findOne(Channel, { relations: ['owner', 'adminGroup', 'groups'], where: { @@ -32,12 +38,14 @@ export class RemoveGroupFromChannel implements Command { channel.removeGroup(group); const updatedChannel = await transactionManager.save(channel); + if (!updatedChannel) throw new Error('Was not able to update the channel.'); + await AuditChannels.setValue(updatedChannel.id, { event: 'RemoveGroup', user: this.authorizationBag.email, groupId: this.groupId, }); - return updatedChannel; + return this.groupId; } } diff --git a/src/services/impl/channels/remove-user-from-channel.ts b/src/services/impl/channels/remove-user-from-channel.ts index 6c8c12cf33ca8aef60de99f7dcaaf4cca7744485..ae9bbe7ecbeed43ce7d2e94c13e1194ca9542241 100644 --- a/src/services/impl/channels/remove-user-from-channel.ts +++ b/src/services/impl/channels/remove-user-from-channel.ts @@ -5,11 +5,12 @@ import { User } from '../../../models/user'; import { ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; import { AuditChannels } from '../../../log/auditing'; +import { ChannelResponse } from "../../../controllers/channels/dto"; export class RemoveUserFromChannel implements Command { constructor(private memberId: string, private channelId: string, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager) { + async execute(transactionManager: EntityManager): Promise<String> { const channel = await transactionManager.findOne( Channel, { id: this.channelId }, @@ -39,6 +40,6 @@ export class RemoveUserFromChannel implements Command { memberId: this.memberId, }); - return channel; + return user.id; } } diff --git a/src/services/impl/channels/set-category.ts b/src/services/impl/channels/set-category.ts index 912e2ef0d2ebba86912b930024316bc156feeb3c..24119257003fbcff79c12d82fed1e2176efbeaac 100644 --- a/src/services/impl/channels/set-category.ts +++ b/src/services/impl/channels/set-category.ts @@ -5,11 +5,12 @@ import { Category } from '../../../models/category'; import { ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; import { AuditChannels } from '../../../log/auditing'; +import { ChannelResponse } from "../../../controllers/channels/dto"; export class SetCategory implements Command { constructor(private channelId: string, private category: Category, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager): Promise<Channel> { + async execute(transactionManager: EntityManager): Promise<ChannelResponse> { let channel = await transactionManager.findOne(Channel, { relations: ['owner', 'adminGroup', 'category'], where: { @@ -40,6 +41,6 @@ export class SetCategory implements Command { category: this.category, }); - return channel; + return new ChannelResponse(channel); } } diff --git a/src/services/impl/channels/set-tags.ts b/src/services/impl/channels/set-tags.ts index a07c8ac1060c31b53114e7ded798ddd4639e3995..e9e34b72c3a146b471c30d9744dbd9ca150be433 100644 --- a/src/services/impl/channels/set-tags.ts +++ b/src/services/impl/channels/set-tags.ts @@ -2,16 +2,20 @@ import { Command } from '../command'; import { EntityManager, In } from 'typeorm'; import { Channel } from '../../../models/channel'; import { Tag } from '../../../models/tag'; -import { ForbiddenError, NotFoundError } from 'routing-controllers'; +import { BadRequestError, ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; -import { isUUID } from 'class-validator'; import { AuditChannels } from '../../../log/auditing'; +import { ChannelResponse, setTagsRequest } from '../../../controllers/channels/dto'; +import { isUUID } from 'class-validator'; export class SetTags implements Command { - constructor(private channelId: string, private tags: Tag[], private authorizationBag: AuthorizationBag) {} + constructor(private channelId: string, private tags: setTagsRequest, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager): Promise<Channel> { - let channel = await transactionManager.findOne(Channel, { + async execute(transactionManager: EntityManager): Promise<ChannelResponse> { + if (!isUUID(this.channelId)) + throw new BadRequestError('Provided channel id: "' + this.channelId + '" is not a UUID.'); + + const channel = await transactionManager.findOne(Channel, { relations: ['owner', 'adminGroup', 'tags'], where: { id: this.channelId, @@ -24,20 +28,15 @@ export class SetTags implements Command { if (!(await channel.isAdmin(this.authorizationBag))) throw new ForbiddenError("You don't have the rights to edit this channel."); - if (this.tags) { - const tagCleaned = this.tags.map(data => { - const tagid = data.id || data; - if (isUUID(tagid as string)) return tagid; - }); - - channel.tags = await transactionManager.find(Tag, { - where: { id: In(tagCleaned) }, - }); - } else channel.tags = null; + this.tags + ? (channel.tags = await transactionManager.find(Tag, { + where: { id: In(this.tags.tagIds) }, + })) + : (channel.tags = null); await transactionManager.save(channel); await AuditChannels.setValue(channel.id, { event: 'AddTags', user: this.authorizationBag.email, tags: this.tags }); - return channel; + return new ChannelResponse(channel); } } diff --git a/src/services/impl/channels/subscribe-to-channel.ts b/src/services/impl/channels/subscribe-to-channel.ts index e4ff134e6976e43ed66d05ea4947b3fd91e7efe6..ba14e453c082cbd9406ddb25460edf8d37a27ad8 100644 --- a/src/services/impl/channels/subscribe-to-channel.ts +++ b/src/services/impl/channels/subscribe-to-channel.ts @@ -5,11 +5,12 @@ import { Channel } from '../../../models/channel'; import { ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; import { AuditChannels } from '../../../log/auditing'; +import { ChannelResponse } from "../../../controllers/channels/dto"; export class SubscribeToChannel implements Command { constructor(private memberId: string, private channelId: string, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager) { + async execute(transactionManager: EntityManager): Promise<ChannelResponse> { const channel = await transactionManager.findOne( Channel, { id: this.channelId }, @@ -37,6 +38,6 @@ export class SubscribeToChannel implements Command { memberId: this.memberId, }); - return updatedChannel; + return new ChannelResponse(updatedChannel); } } diff --git a/src/services/impl/channels/unsubscribe-from-channel.ts b/src/services/impl/channels/unsubscribe-from-channel.ts index 3a01eb80488afbb60daf83f92240181e33f2ffc5..61f14b7ebe7670513980dd080d0c3e0c7fa3e1fa 100644 --- a/src/services/impl/channels/unsubscribe-from-channel.ts +++ b/src/services/impl/channels/unsubscribe-from-channel.ts @@ -6,11 +6,12 @@ import { ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; import { AuditChannels } from '../../../log/auditing'; import { ChannelFlags } from '../../../models/channel-enums'; +import { ChannelResponse } from "../../../controllers/channels/dto"; export class UnsubscribeFromChannel implements Command { constructor(private memberId: string, private channelId: string, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager) { + async execute(transactionManager: EntityManager): Promise<ChannelResponse>{ const channel = await transactionManager.findOne( Channel, { id: this.channelId }, @@ -43,6 +44,6 @@ export class UnsubscribeFromChannel implements Command { memberId: this.memberId, }); - return updatedChannel; + return new ChannelResponse(updatedChannel); } } diff --git a/src/services/impl/channels/update-channel-admin-group.ts b/src/services/impl/channels/update-channel-admin-group.ts index da76b014005ec349df72d738bb587ac399c86ec7..64904672a0c81c7d80b9946deaeadd7fb008e111 100644 --- a/src/services/impl/channels/update-channel-admin-group.ts +++ b/src/services/impl/channels/update-channel-admin-group.ts @@ -1,21 +1,25 @@ -import { Command } from '../command'; -import { EntityManager } from 'typeorm'; -import { Channel } from '../../../models/channel'; -import { Group } from '../../../models/group'; -import { ForbiddenError, NotFoundError } from 'routing-controllers'; -import { AuthorizationBag } from '../../../models/authorization-bag'; -import { AuditChannels } from '../../../log/auditing'; +import { Command } from "../command"; +import { EntityManager } from "typeorm"; +import { Channel } from "../../../models/channel"; +import { Group } from "../../../models/group"; +import { ForbiddenError, NotFoundError } from "routing-controllers"; +import { AuthorizationBag } from "../../../models/authorization-bag"; +import { ChannelResponse } from "../../../controllers/channels/dto"; +import { AuditChannels } from "../../../log/auditing"; export class UpdateChannelAdminGroup implements Command { - constructor(private group: Group, private channelId: string, private authorizationBag: AuthorizationBag) {} - - async execute(transactionManager: EntityManager): Promise<Channel> { - const channel = await transactionManager.findOne(Channel, { - relations: ['owner', 'adminGroup'], - where: { - id: this.channelId, - }, - }); + constructor(private groupName: string, private channelId: string, private authorizationBag: AuthorizationBag) { } + + async execute(transactionManager: EntityManager): Promise<ChannelResponse> { + const channel = await transactionManager.findOne( + Channel, + { + relations: ["owner", "adminGroup"], + where: { + id: this.channelId, + }, + } + ); if (!channel) throw new NotFoundError('Channel does not exist'); @@ -23,12 +27,14 @@ export class UpdateChannelAdminGroup implements Command { if (!(await channel.isAdmin(this.authorizationBag))) throw new ForbiddenError("You don't have the rights to manage this channel."); - if (this.group.groupIdentifier) { + let newGroup = new Group({groupIdentifier: this.groupName}); + + if (!(this.groupName === "" || this.groupName === null)) { let groupToAdd = await transactionManager.findOne(Group, { - groupIdentifier: this.group.groupIdentifier, + groupIdentifier: this.groupName, }); - if (!groupToAdd && (await this.group.exists())) { - groupToAdd = this.group; + if (!groupToAdd && (await newGroup.exists())) { + groupToAdd = newGroup; groupToAdd = await transactionManager.save(groupToAdd); } if (!groupToAdd) throw new NotFoundError('The group does not exist'); @@ -45,9 +51,9 @@ export class UpdateChannelAdminGroup implements Command { await AuditChannels.setValue(updatedChannel.id, { event: 'SetAdminGroup', user: this.authorizationBag.email, - groupIdentifier: this.group.groupIdentifier, + groupIdentifier: this.groupName, }); - return updatedChannel; + return new ChannelResponse(updatedChannel); } } diff --git a/src/services/impl/channels/update-channel.ts b/src/services/impl/channels/update-channel.ts index 721c811f935bd3ba31a4a27b3ceb41c19d567343..a2b65c6347f5a40984f85d3c322a8715862540c2 100644 --- a/src/services/impl/channels/update-channel.ts +++ b/src/services/impl/channels/update-channel.ts @@ -1,27 +1,30 @@ import { Command } from '../command'; -import { EntityManager, Not, In } from 'typeorm'; -import { isUUID, validate } from 'class-validator'; +import { EntityManager, Not } from 'typeorm'; import { Channel } from '../../../models/channel'; -import { Tag } from '../../../models/tag'; -import { Visibility, SubscriptionPolicy } from '../../../models/channel-enums'; import { BadRequestError, ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; import { prepareValidationErrorList } from './validation-utils'; import { AuditChannels } from '../../../log/auditing'; +import { UpdateChannelRequest, ChannelResponse } from '../../../controllers/channels/dto'; +import { validate } from 'class-validator'; export class UpdateChannel implements Command { - constructor(private channel: Channel, private authorizationBag: AuthorizationBag) { - if (channel.visibility === Visibility.restricted) channel.subscriptionPolicy === SubscriptionPolicy.dynamic; - } + constructor(private channel: UpdateChannelRequest, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager): Promise<Channel> { - let channel = await transactionManager.findOne(Channel, { + async execute(transactionManager: EntityManager): Promise<ChannelResponse> { + const channel = await transactionManager.findOne(Channel, { relations: ['adminGroup', 'groups', 'owner'], where: { id: this.channel.id, }, }); + validate(channel).then(errors => { + console.log(errors); + }); + + if (!channel) throw new NotFoundError('The channel does not exist.'); + this.channel.name = this.channel.name.trim(); if (this.channel.name.length < 4 || this.channel.name.length > 128) { @@ -31,8 +34,6 @@ export class UpdateChannel implements Command { throw new BadRequestError("Channel's description should be less than 256 characters."); } - if (!channel) throw new NotFoundError('The channel does not exist.'); - if ( await transactionManager.findOne( Channel, @@ -47,20 +48,6 @@ export class UpdateChannel implements Command { ) { throw new BadRequestError('Channel name already exists'); } - if ( - await transactionManager.findOne( - Channel, - { - id: Not(channel.id), - slug: this.channel.slug, - }, - { - withDeleted: true, - }, - ) - ) { - throw new BadRequestError('Channel slug already exists'); - } // you need to be channel admin if (!(await channel.isAdmin(this.authorizationBag))) @@ -68,10 +55,10 @@ export class UpdateChannel implements Command { // TODO Loop should one day keep only updated properties // but it impacts the returned object. To link with DTOs - for (let key in this.channel) { + for (const key in this.channel) { if (key != 'tags') channel[key] = this.channel[key]; } - + /* if (this.channel.tags) { const tagCleaned = this.channel.tags.map(data => { const tagid = data.id || data; @@ -83,7 +70,7 @@ export class UpdateChannel implements Command { id: In(tagCleaned), }, }); - } + }*/ if (!this.authorizationBag.isSupporter) channel.channelFlags = undefined; @@ -94,6 +81,6 @@ export class UpdateChannel implements Command { const updatedChannel = await transactionManager.save(channel); await AuditChannels.setValue(updatedChannel.id, { event: 'Update', user: this.authorizationBag.email }); - return updatedChannel; + return new ChannelResponse(updatedChannel); } } diff --git a/src/services/impl/notifications-service-impl.ts b/src/services/impl/notifications-service-impl.ts index f172f8031a6eea5bba001d24cf4a6d1deb3b0f05..9fa1c0a6f50b30cf927231c32dd54a816bdb7b5d 100644 --- a/src/services/impl/notifications-service-impl.ts +++ b/src/services/impl/notifications-service-impl.ts @@ -1,5 +1,4 @@ import { NotificationsService } from '../notifications-service'; -import { Notification } from '../../models/notification'; import { SendNotification } from './notifications/send-notification'; import { FindAllNotifications } from './notifications/find-all-notifications'; import { GetById } from './notifications/get-by-id'; @@ -9,6 +8,7 @@ import { UpdateUserNotification } from './notifications/update-user-notification import { AuthorizationBag } from '../../models/authorization-bag'; import { RetryNotification } from './notifications/retry-notification'; import { GetNotificationResponse, SendNotificationRequest } from '../../controllers/notifications/dto'; +import { NotificationsListResponse, Query } from '../../controllers/channels/dto'; export class NotificationsServiceImpl extends AbstractService implements NotificationsService { sendNotification( @@ -22,7 +22,11 @@ export class NotificationsServiceImpl extends AbstractService implements Notific return this.commandExecutor.execute(new RetryNotification(notificationId, authorizationBag)); } - findAllNotifications(channelId: string, query: any, authorizationBag: AuthorizationBag): Promise<Notification[]> { + findAllNotifications( + channelId: string, + query: Query, + authorizationBag: AuthorizationBag, + ): Promise<NotificationsListResponse> { return this.commandExecutor.execute(new FindAllNotifications(channelId, query, authorizationBag)); } diff --git a/src/services/impl/notifications/find-all-notifications.ts b/src/services/impl/notifications/find-all-notifications.ts index 39aeeb243ac7fca43c8c916e94810cb2728afad1..64ba0d6dcea9aab09168f386ff53b111a613eab5 100644 --- a/src/services/impl/notifications/find-all-notifications.ts +++ b/src/services/impl/notifications/find-all-notifications.ts @@ -5,11 +5,12 @@ import { Channel } from '../../../models/channel'; import { ForbiddenError, NotFoundError } from 'routing-controllers'; import { AuthorizationBag } from '../../../models/authorization-bag'; import { CernAuthorizationService } from '../../../models/cern-authorization-service'; +import { NotificationsListResponse, Query } from '../../../controllers/channels/dto'; export class FindAllNotifications implements Command { - constructor(private channelId: string, private query: any, private authorizationBag: AuthorizationBag) {} + constructor(private channelId: string, private query: Query, private authorizationBag: AuthorizationBag) {} - async execute(transactionManager: EntityManager) { + async execute(transactionManager: EntityManager): Promise<NotificationsListResponse> { if (this.authorizationBag) console.time('get-all-notifications:total:' + this.authorizationBag.userName); if (this.authorizationBag) console.time('get-all-notifications:grappa:' + this.authorizationBag.userName); @@ -114,8 +115,8 @@ export class FindAllNotifications implements Command { .orderBy('notification.sentAt', 'DESC', 'NULLS FIRST') .addOrderBy('notification.sendAt', 'ASC') .distinct() - .limit(parseInt(this.query.skip) + parseInt(this.query.take) || 10) - .offset(parseInt(this.query.skip) || 0); + .limit(this.query.skip + this.query.take || 10) + .offset(this.query.skip || 0); const notifications_ids = await notificationQB.getRawMany(); diff --git a/src/services/notifications-service.ts b/src/services/notifications-service.ts index 46cce9eda6bb9960c0d04460822a904b33b150f8..f1a58880496e970d175d3858365d72e2ba1036ee 100644 --- a/src/services/notifications-service.ts +++ b/src/services/notifications-service.ts @@ -1,7 +1,7 @@ -import { Notification } from '../models/notification'; import { UserNotification } from '../models/user-notification'; import { AuthorizationBag } from '../models/authorization-bag'; import { GetNotificationResponse, SendNotificationRequest } from '../controllers/notifications/dto'; +import { NotificationsListResponse, Query } from '../controllers/channels/dto'; export interface NotificationsService { sendNotification( @@ -11,7 +11,11 @@ export interface NotificationsService { retryNotification(notificationId: string, authorizationBag: AuthorizationBag): Promise<void>; - findAllNotifications(channelId: string, query: any, authorizationBag: AuthorizationBag): Promise<Notification[]>; + findAllNotifications( + channelId: string, + query: Query, + authorizationBag: AuthorizationBag, + ): Promise<NotificationsListResponse>; getById(notificationId: string, authorizationBag: AuthorizationBag): Promise<GetNotificationResponse>; diff --git a/src/utils/status-codes.ts b/src/utils/status-codes.ts index f5648507adaa92b5560a8e05c6510e206451b100..1c3103d862f4078b4750bef9df808854767e9053 100644 --- a/src/utils/status-codes.ts +++ b/src/utils/status-codes.ts @@ -57,7 +57,7 @@ export const enum StatusCodes { MisdirectedRequest = '421', } -export const StatusCodeDescriptions: Record<string, object> = { +export const StatusCodeDescriptions: Record<string, unknown> = { [StatusCodes.Accepted]: { description: 'Accepted' }, [StatusCodes.BadGateway]: { description: 'Bad Gateway' }, [StatusCodes.BadRequest]: { description: 'Bad Request' },