<file_sep>using System.Collections.Generic;
using System.Threading.Tasks;
using Tasks.Api.Entities;

namespace Tasks.Api.Data.Interfaces
{
    public interface IRoomRoleRepository : IRepository<RoomRole>
    {
        Task<IEnumerable<RoomRole>> FindAllRoles();

        Task<RoomRole> FindWithName(string roleName);
    }
}<file_sep>﻿using Microsoft.AspNetCore.Http;

namespace Tasks.Api.Exceptions
{
    public class AccessRightApiException : ApiException
    {
        public AccessRightApiException(string message) : base(message)
        {
        }

        public override int StatusCode => StatusCodes.Status403Forbidden;
    }
}<file_sep>﻿using System;

namespace Tasks.Api.Exceptions
{
    [Serializable]
    public abstract class ApiException : Exception
    {
        protected ApiException(string message) : base(message)
        {
        }

        public abstract int StatusCode { get; }
    }
}<file_sep>﻿using Microsoft.EntityFrameworkCore;

namespace Users.Api.Data
{
    public class DatabaseInitializer
    {
        private readonly ApplicationContext _applicationContext;

        public DatabaseInitializer(ApplicationContext applicationContext) => _applicationContext = applicationContext;

        public void Initialize() => _applicationContext.Database.Migrate();
    }
}<file_sep>﻿using System.Collections.Generic;

namespace Tasks.Api.Entities
{
    public class Roles
    {
        public const string Owner = "Owner";

        public const string Administrator = "Admin";

        public const string Member = "Member";

        public static IEnumerable<string> AllRoles()
        {
            yield return Owner;
            yield return Administrator;
            yield return Member;
        }
    }
}<file_sep>﻿using System;

namespace Tasks.Api.Entities
{
    public class UserInTheRoom
    {
        public Guid RoomId { get; set; }

        public Room Room { get; set; }

        public Guid UserId { get; set; }

        public User User { get; set; }

        public Guid RoomRoleId { get; set; }

        public RoomRole RoomRole { get; set; }
    }
}<file_sep>﻿using System.ComponentModel.DataAnnotations;

namespace Tasks.Api.ViewModels.RoomViewModels
{
    public class AddOrUpdateRoomViewModel
    {
        [StringLength(30)]
        [Required]
        public string RoomName { get; set; }

        public string? RoomDescription { get; set; }
    }
}<file_sep>using AutoMapper;
using Tasks.Api.Entities;
using Tasks.Api.ViewModels.RoleViewModels;

namespace Tasks.Api.Profiles
{
    public class RoleProfile : Profile
    {
        public RoleProfile()
        {
            CreateMap<RoomRole, RoleViewModel>();
        }
    }
}<file_sep>using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using IdentityServer4;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Identity;
using Users.Data.Entities;

namespace IdentityServer
{
    public class ProfileService : IProfileService
    {
        private readonly IUserClaimsPrincipalFactory<User> _claimsFactory;

        private readonly UserManager<User> _userManager;

        public ProfileService(IUserClaimsPrincipalFactory<User> claimsFactory, UserManager<User> userManager)
        {
            _claimsFactory = claimsFactory;
            _userManager = userManager;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var user = await _userManager.FindByIdAsync(context.Subject.GetSubjectId());
            var principal = await _claimsFactory.CreateAsync(user);

            var claims = principal.Claims.ToList();
            claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();

            if (context.Caller == IdentityServerConstants.ProfileDataCallers.UserInfoEndpoint &&
                context.RequestedClaimTypes.Contains("full_name"))
            {
                claims.Add(new Claim("last_name", user.LastName));
                claims.Add(new Claim("first_name", user.FirstName));
                claims.Add(new Claim("patronymic", user.Patronymic));
            }

            context.IssuedClaims = claims;
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            var user = await _userManager.FindByIdAsync(context.Subject.GetSubjectId());
            context.IsActive = user != null;
        }
    }
}<file_sep>using System;

namespace Users.Data
{
    public class UserRegistered
    {
        public Guid UserId { get; set; }

        public string LastName { get; set; }

        public string FirstName { get; set; }

        public string Patronymic { get; set; }
    }
}<file_sep>﻿using System;
using System.ComponentModel.DataAnnotations;

namespace Tasks.Api.ViewModels.TaskViewModels
{
    public class AddTaskViewModel
    {
        [Required]
        [StringLength(30, MinimumLength = 1)]
        public string TaskName { get; set; }

        [Required]
        [MinLength(1)]
        public string? TaskContent { get; set; }

        public string? Details { get; set; }

        public DateTime DeadlineTime { get; set; }
    }
}<file_sep>﻿using System;
using Microsoft.EntityFrameworkCore.Migrations;

namespace Tasks.Api.Migrations
{
    public partial class FixedIncorrectRelatioRoomTaskWithUser : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropForeignKey(
                name: "FK_RoomTasks_UserInTheRoom_TaskCreatorRoomId_TaskCreatorUserId",
                table: "RoomTasks");

            migrationBuilder.DropIndex(
                name: "IX_RoomTasks_TaskCreatorRoomId_TaskCreatorUserId",
                table: "RoomTasks");

            migrationBuilder.DropColumn(
                name: "TaskCreatorRoomId",
                table: "RoomTasks");

            migrationBuilder.DropColumn(
                name: "TaskCreatorUserId",
                table: "RoomTasks");

            migrationBuilder.CreateIndex(
                name: "IX_RoomTasks_TaskCreatorId",
                table: "RoomTasks",
                column: "TaskCreatorId");

            migrationBuilder.AddForeignKey(
                name: "FK_RoomTasks_Users_TaskCreatorId",
                table: "RoomTasks",
                column: "TaskCreatorId",
                principalTable: "Users",
                principalColumn: "UserId",
                onDelete: ReferentialAction.Cascade);
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropForeignKey(
                name: "FK_RoomTasks_Users_TaskCreatorId",
                table: "RoomTasks");

            migrationBuilder.DropIndex(
                name: "IX_RoomTasks_TaskCreatorId",
                table: "RoomTasks");

            migrationBuilder.AddColumn<Guid>(
                name: "TaskCreatorRoomId",
                table: "RoomTasks",
                type: "uuid",
                nullable: true);

            migrationBuilder.AddColumn<Guid>(
                name: "TaskCreatorUserId",
                table: "RoomTasks",
                type: "uuid",
                nullable: true);

            migrationBuilder.CreateIndex(
                name: "IX_RoomTasks_TaskCreatorRoomId_TaskCreatorUserId",
                table: "RoomTasks",
                columns: new[] { "TaskCreatorRoomId", "TaskCreatorUserId" });

            migrationBuilder.AddForeignKey(
                name: "FK_RoomTasks_UserInTheRoom_TaskCreatorRoomId_TaskCreatorUserId",
                table: "RoomTasks",
                columns: new[] { "TaskCreatorRoomId", "TaskCreatorUserId" },
                principalTable: "UserInTheRoom",
                principalColumns: new[] { "RoomId", "UserId" },
                onDelete: ReferentialAction.Restrict);
        }
    }
}
<file_sep>using System;

namespace Tasks.Api.ViewModels.RoleViewModels
{
    public class RoleViewModel
    {
        public Guid RoomRoleId { get; set; }

        public string RoomRoleName { get; set; }

        public string? RoomRoleDescription { get; set; }
    }
}<file_sep>using System;

namespace Users.Api.Exceptions
{
    public abstract class ApiException : Exception
    {
        public abstract int StatusCode { get; }
    }

    public abstract class ApiException<T> : ApiException
    {
        protected ApiException(T errorData) => ErrorData = errorData;
        public T ErrorData { get; set; }
    }
}<file_sep>﻿using System;
using Microsoft.EntityFrameworkCore.Migrations;

namespace Tasks.Api.Migrations
{
    public partial class UserNowCanHaveOnlyOneRole : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "RoomRoleUserInRoom");

            migrationBuilder.DropColumn(
                name: "OwnerId",
                table: "Rooms");

            migrationBuilder.AddColumn<Guid>(
                name: "RoomRoleId",
                table: "UsersInRoom",
                type: "uuid",
                nullable: false,
                defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));

            migrationBuilder.CreateIndex(
                name: "IX_UsersInRoom_RoomRoleId",
                table: "UsersInRoom",
                column: "RoomRoleId");

            migrationBuilder.AddForeignKey(
                name: "FK_UsersInRoom_RoomRoles_RoomRoleId",
                table: "UsersInRoom",
                column: "RoomRoleId",
                principalTable: "RoomRoles",
                principalColumn: "RoomRoleId",
                onDelete: ReferentialAction.Cascade);
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropForeignKey(
                name: "FK_UsersInRoom_RoomRoles_RoomRoleId",
                table: "UsersInRoom");

            migrationBuilder.DropIndex(
                name: "IX_UsersInRoom_RoomRoleId",
                table: "UsersInRoom");

            migrationBuilder.DropColumn(
                name: "RoomRoleId",
                table: "UsersInRoom");

            migrationBuilder.AddColumn<Guid>(
                name: "OwnerId",
                table: "Rooms",
                type: "uuid",
                nullable: false,
                defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));

            migrationBuilder.CreateTable(
                name: "RoomRoleUserInRoom",
                columns: table => new
                {
                    RoomRolesRoomRoleId = table.Column<Guid>(type: "uuid", nullable: false),
                    UserInRoomsRoomId = table.Column<Guid>(type: "uuid", nullable: false),
                    UserInRoomsUserId = table.Column<Guid>(type: "uuid", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_RoomRoleUserInRoom", x => new { x.RoomRolesRoomRoleId, x.UserInRoomsRoomId, x.UserInRoomsUserId });
                    table.ForeignKey(
                        name: "FK_RoomRoleUserInRoom_RoomRoles_RoomRolesRoomRoleId",
                        column: x => x.RoomRolesRoomRoleId,
                        principalTable: "RoomRoles",
                        principalColumn: "RoomRoleId",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_RoomRoleUserInRoom_UsersInRoom_UserInRoomsRoomId_UserInRoom~",
                        columns: x => new { x.UserInRoomsRoomId, x.UserInRoomsUserId },
                        principalTable: "UsersInRoom",
                        principalColumns: new[] { "RoomId", "UserId" },
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                name: "IX_RoomRoleUserInRoom_UserInRoomsRoomId_UserInRoomsUserId",
                table: "RoomRoleUserInRoom",
                columns: new[] { "UserInRoomsRoomId", "UserInRoomsUserId" });
        }
    }
}
<file_sep>﻿using System;

namespace Tasks.Api.ViewModels.TaskViewModels
{
    public class TaskViewModel
    {
        public Guid RoomTaskId { get; set; }

        public string TaskName { get; set; }

        public string TaskContent { get; set; }

        public string Details { get; set; }
        
        public bool IsCompleted { get; set; }

        public DateTime TaskCreationTime { get; set; } = DateTime.Now;

        public Guid TaskCreatorId { get; set; }

        public DateTime DeadlineTime { get; set; }
    }
}<file_sep>﻿using System;
using System.Linq.Expressions;
using System.Threading.Tasks;

namespace Tasks.Api.Data
{
    public interface IRepository<T> where T : class
    {
        public Task<T> Create(T element);

        public Task<T?> Find(Expression<Func<T, bool>> expression);

        public void Update(T element);

        public void Delete(T element);
    }
}<file_sep>using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using IdentityServer4.AccessTokenValidation;
using MassTransit;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Tasks.Api.Data;
using Tasks.Api.MassTransit;
using Tasks.Api.Services;

namespace Tasks.Api
{
    public class Startup
    {
        private readonly IConfiguration _configuration;

        public Startup(IConfiguration configuration) => _configuration = configuration;

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationContext>(options =>
            {
                options.UseNpgsql(_configuration.GetConnectionString("TasksDb") + _configuration["DbName"]);
            });

            services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
                .AddIdentityServerAuthentication(options =>
                {
                    options.Authority = _configuration["IdentityServer:Authority"];
                    options.RequireHttpsMetadata = false;
                });

            services.AddSwaggerGen(options =>
            {
                options.EnableAnnotations();

                options.SwaggerDoc("v1", new OpenApiInfo
                {
                    Title = "RoomsApi",
                    Description = "Service for managing rooms and tasks",
                    Version = "v1",

                    Contact = new OpenApiContact
                    {
                        Email = "<EMAIL>",
                        Name = "<NAME>"
                    }
                });

                options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.OAuth2,

                    Flows = new OpenApiOAuthFlows
                    {
                        Password = new OpenApiOAuthFlow
                        {
                            TokenUrl = new Uri(_configuration["IdentityServer:TokenUrl"])
                        }
                    }
                });

                options.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "oauth2"
                            },
                            Scheme = "oauth2",
                            Name = "Bearer",
                            In = ParameterLocation.Header
                        },
                        new List<string>()
                    }
                });

                string xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                string filePath = Path.Combine(AppContext.BaseDirectory, xmlFile);
                options.IncludeXmlComments(filePath);
            });

            services.AddAutoMapper(Assembly.GetExecutingAssembly());

            services.AddScoped<UnitOfWork>();

            services.AddScoped<UserService>();
            services.AddScoped<RoomService>();
            services.AddScoped<RoomTaskService>();
            services.AddScoped<RoleService>();

            services.AddTransient<DatabaseInitializer>();

            services.AddControllers();

            services.AddMassTransit(x =>
            {
                x.AddConsumer<UserRegisteredConsumer>();
                x.AddConsumer<ProfileUpdatedConsumer>();

                x.UsingRabbitMq((context, configurator) =>
                {
                    configurator.Host(_configuration["MassTransit:Host"],
                        _configuration["MassTransit:VirtualHost"], options =>
                        {
                            options.Username(_configuration["MassTransit:Username"]);
                            options.Password(_configuration["MassTransit:Password"]);
                        });

                    configurator.ReceiveEndpoint("registered-users",
                        e => { e.ConfigureConsumer<UserRegisteredConsumer>(context); });

                    configurator.ReceiveEndpoint("updated-profiles",
                        e => { e.ConfigureConsumer<ProfileUpdatedConsumer>(context); });
                });
            });


            services.AddMassTransitHostedService();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DatabaseInitializer databaseInitializer)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            databaseInitializer.Initialize();

            app.UseSwagger();
            app.UseSwaggerUI(options =>
            {
                options.RoutePrefix = "";
                options.SwaggerEndpoint("/swagger/v1/swagger.json", "TasksApi");

                options.DocumentTitle = "TasksApi";

                options.OAuthClientId(_configuration["IdentityServer:ClientId"]);
                options.OAuthClientSecret(_configuration["IdentityServer:Secret"]);
            });

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseMiddleware<ExceptionMiddleware>();

            app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
        }
    }
}<file_sep>using IdentityServer.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Users.Data.Entities;

namespace IdentityServer
{
    public class Startup
    {
        private readonly IConfiguration _configuration;

        public Startup(IConfiguration configuration) => _configuration = configuration;

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationContext>(options =>
                {
                    options.UseNpgsql(_configuration.GetConnectionString("Identity") + _configuration["DbName"]);
                })
                .AddIdentity<User, Role>()
                .AddEntityFrameworkStores<ApplicationContext>();

            services.AddIdentityServer(options => { options.IssuerUri = _configuration["IssuerUri"]; })
                .AddInMemoryApiScopes(Config.ApiScopes)
                .AddInMemoryClients(Config.GetConfiguredClients(_configuration))
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddAspNetIdentity<User>()
                .AddProfileService<ProfileService>()
                .AddDeveloperSigningCredential();

            services.AddControllers();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseIdentityServer();

            app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
        }
    }
}<file_sep>using System;
using Tasks.Api.ViewModels.UserViewModel;

namespace Tasks.Api.ViewModels.RoleViewModels
{
    public class UserWithRoleViewModel
    {
        public UserFullNameViewModel User { get; set; }

        public Guid RoleId { get; set; }
    }
}<file_sep>﻿using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using Tasks.Api.Data;
using Tasks.Api.Entities;
using Tasks.Api.Exceptions;
using Tasks.Api.ViewModels.RoomViewModels;

namespace Tasks.Api.Services
{
    public class RoomService
    {
        private readonly IMapper _mapper;

        private readonly UnitOfWork _unitOfWork;

        private readonly UserService _userService;

        public RoomService(IMapper mapper, UnitOfWork unitOfWork, UserService userService)
        {
            _mapper = mapper;
            _unitOfWork = unitOfWork;
            _userService = userService;
        }

        public async Task<RoomViewModel> CreateRoom(AddOrUpdateRoomViewModel viewModel, Guid userId)
        {
            var room = _mapper.Map<Room>(viewModel);
            room.UsersInRoom = new List<UserInTheRoom>
            {
                new()
                {
                    UserId = userId,
                    RoomRole = await _unitOfWork.RoomRoleRepository.FindWithName(Roles.Owner)
                }
            };

            var createdRoom = await _unitOfWork.RoomRepository.Create(room);
            await _unitOfWork.SaveChangesAsync();
            return _mapper.Map<RoomViewModel>(createdRoom);
        }

        public async Task UpdateRoom(AddOrUpdateRoomViewModel viewModel, Guid roomId, Guid userId)
        {
            var room = await _unitOfWork.RoomRepository.Find(x => x.RoomId == roomId);

            if (room == null)
                throw new NotFoundApiException(AppExceptions.RoomNotFoundException);

            if (!await _userService.CheckUserHasAnyRole(roomId, userId, Roles.Owner, Roles.Administrator))
                throw new AccessRightApiException(AppExceptions.CreatorOrAdministratorOnlyCanDoThisException);

            room = _mapper.Map(viewModel, room);

            _unitOfWork.RoomRepository.Update(room);
            await _unitOfWork.SaveChangesAsync();
        }

        public async Task DeleteRoom(Guid roomId, Guid userId)
        {
            var room = await _unitOfWork.RoomRepository.Find(x => x.RoomId == roomId);

            if (room == null)
                throw new NotFoundApiException(AppExceptions.RoomNotFoundException);

            if (!await _userService.CheckUserIsInRole(roomId, userId, Roles.Owner))
                throw new AccessRightApiException(AppExceptions.CreatorOnlyCanPerformThisActionException);

            _unitOfWork.RoomRepository.Delete(room);
            await _unitOfWork.SaveChangesAsync();
        }

        public async Task<IEnumerable<RoomViewModel>> FindRoomsForUser(Guid userId)
        {
            var rooms = await _unitOfWork.RoomRepository.FindRoomsForUser(userId);
            return _mapper.Map<IEnumerable<RoomViewModel>>(rooms);
        }

        public async Task<Room?> FindRoomWithUsers(Guid roomId) =>
            await _unitOfWork.RoomRepository.FindRoomWithUsers(roomId);

        public async Task<RoomViewModel?> FindById(Guid roomId)
        {
            var room = await _unitOfWork.RoomRepository.Find(x => x.RoomId == roomId);
            return _mapper.Map<RoomViewModel>(room);
        }

        public async Task<RoomViewModel?> JoinUserToRoom(Guid roomId, Guid userId)
        {
            var room = await _unitOfWork.RoomRepository.FindRoomWithUsers(roomId);

            if (room == null)
                throw new NotFoundApiException(AppExceptions.IncorrectUrlException);

            var mappedRoom = _mapper.Map<RoomViewModel>(room);
            if (await _unitOfWork.RoomRepository.FindUserInRoom(roomId, userId) != null)
                throw new AlreadyDoneApiException(AppExceptions.AlreadyMember);

            room.UsersInRoom.Add(new UserInTheRoom
            {
                UserId = userId,
                RoomRole = await _unitOfWork.RoomRoleRepository.FindWithName(Roles.Member)
            });
            await _unitOfWork.SaveChangesAsync();
            return mappedRoom;
        }

        public async Task ThrowIfRoomNotFound(Guid roomId)
        {
            if (await FindById(roomId) == null)
                throw new NotFoundApiException(AppExceptions.RoomNotFoundException);
        }
    }
}<file_sep>using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Tasks.Api.Entities;

namespace Tasks.Api.Data.Interfaces
{
    public interface IRoomTaskRepository : IRepository<RoomTask>
    {
        Task<IEnumerable<RoomTask>> FindTasksInRoom(Guid roomId);
    }
}<file_sep>using System;
using System.Linq;
using System.Threading.Tasks;
using AutoMapper;
using MassTransit;
using Tasks.Api.Data;
using Users.Data;

namespace Tasks.Api.MassTransit
{
    public class ProfileUpdatedConsumer : IConsumer<ProfileUpdated>
    {
        private readonly ApplicationContext _applicationContext;

        private readonly IMapper _mapper;

        public ProfileUpdatedConsumer(IMapper mapper, ApplicationContext applicationContext)
        {
            _mapper = mapper;
            _applicationContext = applicationContext;
        }

        public async Task Consume(ConsumeContext<ProfileUpdated> context)
        {
            var updatedProfile = context.Message;
            var user = _applicationContext.Users.FirstOrDefault(x => x.UserId == updatedProfile.UserId);
            var updatedUser = _mapper.Map(updatedProfile, user);

            _applicationContext.Users.Update(updatedUser ?? throw new Exception("Can`t map updated profile"));
            await _applicationContext.SaveChangesAsync();
        }
    }
}<file_sep>﻿using Microsoft.EntityFrameworkCore;
using Tasks.Api.Entities;

namespace Tasks.Api.Data
{
    public class ApplicationContext : DbContext
    {
        public ApplicationContext(DbContextOptions options) : base(options)
        {
        }

        public DbSet<Room> Rooms { get; set; }

        public DbSet<UserInTheRoom> UsersInRoom { get; set; }

        public DbSet<RoomRole> RoomRoles { get; set; }

        public DbSet<RoomTask> RoomTasks { get; set; }

        public DbSet<User> Users { get; set; }

        public DbSet<UserInTheRoom> UsersInTheRooms { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder
            .Entity<UserInTheRoom>()
            .HasKey(x => new {x.RoomId, RoomUserId = x.UserId});
    }
}<file_sep>﻿using System;
using System.Collections.Generic;

namespace Tasks.Api.Entities
{
    public class Room
    {
        public Guid RoomId { get; set; }

        public string RoomName { get; set; }

        public string? RoomDescription { get; set; }

        public DateTime CreationDate { get; set; } = DateTime.Now;

        public List<UserInTheRoom> UsersInRoom { get; set; }

        public List<RoomTask> RoomTasks { get; set; }
    }
}<file_sep>﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using Tasks.Api.Services;
using Tasks.Api.ViewModels.TaskViewModels;
using Tasks.Api.ViewModels.UserViewModel;

namespace Tasks.Api.Controllers
{
    /// <summary>
    ///     Everything about tasks
    /// </summary>
    [Authorize]
    [ApiController]
    [Route("api/room/{roomId:guid}/[controller]")]
    [SwaggerTag("Everything about tasks")]
    public class TasksController : ControllerBase
    {
        private readonly RoomTaskService _roomTaskService;

        /// <inheritdoc />
        public TasksController(RoomTaskService roomTaskService) => _roomTaskService = roomTaskService;

        /// <summary>
        ///     Finds all tasks in room
        /// </summary>
        /// <param name="roomId"></param>
        /// <returns></returns>
        [HttpGet]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status204NoContent)]
        [SwaggerResponse(StatusCodes.Status403Forbidden)]
        [SwaggerResponse(StatusCodes.Status404NotFound)]
        [ProducesDefaultResponseType]
        public async Task<ActionResult<IEnumerable<TaskViewModel>>> AllTasksInRoom(Guid roomId)
        {
            var tasks = await _roomTaskService.FindAllTasksInRoom(roomId);
            if (!tasks.Any())
                return NoContent();
            return Ok(tasks);
        }

        /// <summary>
        ///     Finds task in room by id
        /// </summary>
        /// <param name="roomId"></param>
        /// <param name="taskId"></param>
        /// <returns></returns>
        [HttpGet("{taskId:guid}")]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status403Forbidden)]
        [SwaggerResponse(StatusCodes.Status404NotFound)]
        public async Task<ActionResult<TaskViewModel>> Find(Guid roomId, Guid taskId)
        {
            var task = await _roomTaskService.FindTask(roomId, taskId, UserService.GetCurrentUserId(HttpContext));
            if (task == null)
                return NotFound();
            return Ok(task);
        }

        /// <summary>
        ///     Finds creator of the task in room
        /// </summary>
        /// <param name="roomId"></param>
        /// <param name="taskId"></param>
        /// <returns></returns>
        [HttpGet("{taskId:guid}/creator")]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status403Forbidden)]
        [SwaggerResponse(StatusCodes.Status404NotFound)]
        public async Task<ActionResult<UserFullNameViewModel>> FindCreator(Guid roomId, Guid taskId)
        {
            var creator =
                await _roomTaskService.FindTaskCreator(roomId, taskId, UserService.GetCurrentUserId(HttpContext));
            return Ok(creator);
        }

        /// <summary>
        ///     Marks task completed
        /// </summary>
        /// <param name="roomId"></param>
        /// <param name="taskId"></param>
        /// <returns></returns>
        [HttpGet("{taskId:guid}/complete")]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status403Forbidden)]
        [SwaggerResponse(StatusCodes.Status404NotFound)]
        public async Task<ActionResult<UserFullNameViewModel>> CompleteTask(Guid roomId, Guid taskId) => Ok(
            await _roomTaskService.CompleteTask(roomId, taskId, UserService.GetCurrentUserId(HttpContext)));


        /// <summary>
        ///     Creates task
        /// </summary>
        /// <param name="roomId"></param>
        /// <param name="viewModel"></param>
        /// <returns></returns>
        [HttpPost]
        [SwaggerResponse(StatusCodes.Status201Created)]
        [SwaggerResponse(StatusCodes.Status403Forbidden)]
        [SwaggerResponse(StatusCodes.Status404NotFound)]
        public async Task<CreatedAtActionResult> Create(Guid roomId, AddTaskViewModel viewModel)
        {
            var addedTask =
                await _roomTaskService.CreateTask(roomId, viewModel, UserService.GetCurrentUserId(HttpContext));
            return CreatedAtAction(nameof(Find), new {roomId = addedTask.RoomTaskId, taskId = addedTask.RoomTaskId},
                addedTask);
        }

        /// <summary>
        ///     Updates task by id
        /// </summary>
        /// <param name="roomId"></param>
        /// <param name="taskId"></param>
        /// <param name="viewModel"></param>
        /// <returns></returns>
        [HttpPut("{taskId:guid}")]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status403Forbidden)]
        [SwaggerResponse(StatusCodes.Status404NotFound)]
        public async Task<ActionResult> Update(Guid roomId, Guid taskId, AddTaskViewModel viewModel)
        {
            await _roomTaskService.UpdateTask(roomId, taskId, viewModel, UserService.GetCurrentUserId(HttpContext));
            return Ok();
        }

        /// <summary>
        ///     Deletes task by id
        /// </summary>
        /// <param name="roomId"></param>
        /// <param name="taskId"></param>
        /// <returns></returns>
        [HttpDelete("{taskId:guid}")]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status403Forbidden)]
        [SwaggerResponse(StatusCodes.Status404NotFound)]
        public async Task<ActionResult> Delete(Guid roomId, Guid taskId)
        {
            await _roomTaskService.DeleteTask(roomId, taskId, UserService.GetCurrentUserId(HttpContext));
            return Ok();
        }
    }
}<file_sep>namespace Tasks.Api.Exceptions
{
    public static class AppExceptions
    {
        public const string RoomNotFoundException = "Room with given id was not found!";

        public const string TaskNotFoundException = "Task with given id was not found!";

        public const string TaskInRoomNotFoundException = "Task with given id was not found in room!";

        public const string RoleNotFoundException = "Role with given id was not found!";

        private const string InsufficientRightsException = "Insufficient rights! ";

        public const string NotRoomMemberException =
            InsufficientRightsException + "The user is not a member of the room!";

        public const string CreatorOnlyCanPerformThisActionException =
            InsufficientRightsException + "Only owner can perform this action!";

        public const string CreatorOrAdministratorOnlyCanDoThisException =
            InsufficientRightsException + "Only owner or administrator can perform this action!";

        public const string IncorrectUrlException = "Incorrect link!";

        public const string AlreadyMember = "You are already a member of the room!";

        public const string CreatorNotFound = "Creator of the task was not found!";
    }
}<file_sep>﻿using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Tasks.Api.Data;
using Tasks.Api.Exceptions;

namespace Tasks.Api.Services
{
    public class UserService
    {
        private readonly UnitOfWork _unitOfWork;

        public UserService(UnitOfWork unitOfWork) => _unitOfWork = unitOfWork;

        public async Task<bool> CheckUserHasAnyRole(Guid roomId, Guid userId, params string[] rolesNames)
        {
            foreach (string roleName in rolesNames)
            {
                if (await CheckUserIsInRole(roomId, userId, roleName))
                    return true;
            }

            return false;
        }

        public static Guid GetCurrentUserId(HttpContext context) =>
            Guid.Parse(context.User.FindFirstValue("sub") ?? string.Empty);

        public async Task<bool> CheckUserIsInRole(Guid roomId, Guid userId, string roleName) =>
            await _unitOfWork.RoomRepository.FindRoomWithUserInRole(roomId, userId, roleName) != null;

        private async Task<bool> CheckUserIsInRoom(Guid roomId, Guid userId)
        {
            var room = await _unitOfWork.RoomRepository.FindRoomWithUsers(roomId);
            return room?.UsersInRoom.Any(x => x.UserId == userId) ?? false;
        }

        public async Task ThrowIfNotRoomMember(Guid roomId, Guid userId)
        {
            if (!await CheckUserIsInRoom(roomId, userId))
                throw new AccessRightApiException(AppExceptions.NotRoomMemberException);
        }
    }
}<file_sep>using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AutoMapper;
using Tasks.Api.Data;
using Tasks.Api.Entities;
using Tasks.Api.Exceptions;
using Tasks.Api.ViewModels.RoleViewModels;
using Tasks.Api.ViewModels.UserViewModel;

namespace Tasks.Api.Services
{
    public class RoleService
    {
        private readonly IMapper _mapper;

        private readonly RoomService _roomService;

        private readonly UnitOfWork _unitOfWork;

        private readonly UserService _userService;

        public RoleService(UnitOfWork unitOfWork, UserService userService, RoomService roomService, IMapper mapper)
        {
            _unitOfWork = unitOfWork;
            _userService = userService;
            _roomService = roomService;
            _mapper = mapper;
        }

        public async Task<IEnumerable<RoleViewModel>> AllRoles()
        {
            var roles = await _unitOfWork.RoomRoleRepository.FindAllRoles();
            return _mapper.Map<IEnumerable<RoleViewModel>>(roles);
        }

        public async Task UpdateRoleForUser(Guid roomId, UpdateUserRoleViewModel viewModel, Guid userId)
        {
            await _roomService.ThrowIfRoomNotFound(roomId);
            await _userService.ThrowIfNotRoomMember(roomId, userId);

            if (!await _userService.CheckUserIsInRole(roomId, userId, Roles.Owner))
                throw new AccessRightApiException(AppExceptions.CreatorOnlyCanPerformThisActionException);

            if (viewModel.UserId == userId)
                return;

            var role = await _unitOfWork.RoomRoleRepository.Find(x => x.RoomRoleId == viewModel.RoomRoleId);
            if (role == null)
                throw new NotFoundApiException(AppExceptions.RoleNotFoundException);

            var user = await _unitOfWork.RoomRepository.FindUserInRoom(roomId, viewModel.UserId);
            if (user != null)
                user.RoomRole = role;
            await _unitOfWork.SaveChangesAsync();
        }

        public async Task<List<UserWithRoleViewModel>> GetUsersInRoomWithRoles(Guid roomId, Guid userId)
        {
            await _roomService.ThrowIfRoomNotFound(roomId);
            await _userService.ThrowIfNotRoomMember(roomId, userId);

            var room = await _unitOfWork.RoomRepository.FindRoomWithUsers(roomId);
            if (room == null)
                throw new NotFoundApiException(AppExceptions.RoomNotFoundException);

            return room.UsersInRoom.Select(x => new UserWithRoleViewModel
            {
                User = _mapper.Map<UserFullNameViewModel>(x.User),
                RoleId = x.RoomRoleId
            }).ToList();
        }
    }
}<file_sep>﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using Tasks.Api.Services;
using Tasks.Api.ViewModels.RoomViewModels;

namespace Tasks.Api.Controllers
{
    /// <summary>
    ///     Everything about rooms
    /// </summary>
    [Authorize]
    [ApiController]
    [Route("api/[controller]")]
    [SwaggerResponse(StatusCodes.Status401Unauthorized, "If user is unauthorized")]
    [SwaggerTag("Everything about rooms")]
    public class RoomsController : ControllerBase
    {
        private readonly RoomService _roomService;

        /// <inheritdoc />
        public RoomsController(RoomService roomService) => _roomService = roomService;

        /// <summary>
        ///     Finds all rooms for user
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status204NoContent, "If user hasn`t any rooms")]
        public async Task<ActionResult<IEnumerable<RoomViewModel>>> AllRoomsForUser()
        {
            var rooms = await _roomService.FindRoomsForUser(UserService.GetCurrentUserId(HttpContext));
            if (!rooms.Any())
                return NoContent();
            return Ok(rooms);
        }

        /// <summary>
        ///     Updates room information by id
        /// </summary>
        /// <param name="viewModel"></param>
        /// <param name="roomId"></param>
        /// <returns></returns>
        [HttpPut("{roomId:guid}")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status404NotFound, "If id is incorrect", typeof(ProblemDetails))]
        [SwaggerResponse(StatusCodes.Status403Forbidden, "If user has insufficient rights", typeof(ProblemDetails))]
        public async Task<ActionResult> Update(AddOrUpdateRoomViewModel viewModel, Guid roomId)
        {
            await _roomService.UpdateRoom(viewModel, roomId, UserService.GetCurrentUserId(HttpContext));
            return Ok();
        }

        /// <summary>
        ///     Creates new room for user
        /// </summary>
        /// <param name="viewModel"></param>
        /// <returns></returns>
        [HttpPost]
        [SwaggerResponse(StatusCodes.Status201Created, Type = typeof(RoomViewModel))]
        public async Task<CreatedAtActionResult> Create(AddOrUpdateRoomViewModel viewModel)
        {
            var createdRoom = await _roomService.CreateRoom(viewModel, UserService.GetCurrentUserId(HttpContext));
            return CreatedAtAction(nameof(Find), new {roomId = createdRoom.RoomId}, createdRoom);
        }

        /// <summary>
        ///     Deletes room by id
        /// </summary>
        /// <param name="roomId"></param>
        /// <returns></returns>
        [HttpDelete("{roomId:guid}")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status404NotFound, "If id is incorrect", typeof(ProblemDetails))]
        [SwaggerResponse(StatusCodes.Status403Forbidden, "If user has insufficient rights", typeof(ProblemDetails))]
        public async Task<ActionResult> Delete(Guid roomId)
        {
            await _roomService.DeleteRoom(roomId, UserService.GetCurrentUserId(HttpContext));
            return Ok();
        }

        /// <summary>
        ///     Finds room by id
        /// </summary>
        /// <param name="roomId"></param>
        /// <returns></returns>
        [HttpGet("{roomId:guid}")]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status404NotFound, "If id is incorrect", typeof(ProblemDetails))]
        public async Task<ActionResult<RoomViewModel>> Find(Guid roomId)
        {
            var room = await _roomService.FindById(roomId);
            if (room == null)
                return NotFound();
            return Ok(room);
        }

        /// <summary>
        ///     Joins user to new room by id
        /// </summary>
        /// <param name="roomId"></param>
        /// <returns></returns>
        [HttpGet("join/{roomId:guid}")]
        [SwaggerResponse(StatusCodes.Status200OK, type: typeof(RoomViewModel))]
        [SwaggerResponse(StatusCodes.Status404NotFound, "If link is incorrect", typeof(ProblemDetails))]
        public async Task<ActionResult<RoomViewModel>> JoinRoom(Guid roomId)
        {
            var room = await _roomService.JoinUserToRoom(roomId, UserService.GetCurrentUserId(HttpContext));
            return Ok(room);
        }
    }
}<file_sep>﻿using System;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace Tasks.Api.Data
{
    public abstract class Repository<T> : IRepository<T> where T : class
    {
        protected readonly ApplicationContext _context;

        protected Repository(ApplicationContext context) => _context = context;

        public async Task<T> Create(T element)
        {
            var addedElement = await _context.Set<T>().AddAsync(element);
            return addedElement.Entity;
        }

        public async Task<T?> Find(Expression<Func<T, bool>> expression) =>
            await _context.Set<T>().FirstOrDefaultAsync(expression);

        public void Update(T element) => _context.Set<T>().Update(element);

        public void Delete(T element) => _context.Set<T>().Remove(element);
    }
}<file_sep>﻿FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

FROM build AS publish
COPY ./ ./
RUN dotnet publish -c Release -o out

FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
WORKDIR /app
COPY --from=publish /app/out ./

ENTRYPOINT ["dotnet", "Tasks.Api.dll"]

EXPOSE 80
<file_sep>﻿using System.Threading.Tasks;
using Tasks.Api.Data.Interfaces;
using Tasks.Api.Data.Repositories;

namespace Tasks.Api.Data
{
    public class UnitOfWork
    {
        private readonly ApplicationContext _context;

        private IRoomRepository? _roomRepository;

        private IRoomRoleRepository? _roomRoleRepository;

        private IRoomTaskRepository? _roomTaskRepository;

        public UnitOfWork(ApplicationContext context) => _context = context;

        public IRoomRepository RoomRepository => _roomRepository ??= new RoomRepository(_context);

        public IRoomRoleRepository RoomRoleRepository => _roomRoleRepository ??= new RoomRoleRepository(_context);

        public IRoomTaskRepository RoomTaskRepository => _roomTaskRepository ??= new RoomTaskRepository(_context);

        public async Task SaveChangesAsync() => await _context.SaveChangesAsync();
    }
}<file_sep>﻿using System;

namespace Tasks.Api.ViewModels.RoomViewModels
{
    public class RoomViewModel
    {
        public Guid RoomId { get; set; }

        public string RoomName { get; set; }

        public string? RoomDescription { get; set; }

        public DateTime CreationDate { get; set; }
    }
}<file_sep>using Microsoft.AspNetCore.Http;

namespace Users.Api.Exceptions
{
    public class UserNotFoundApiException : ApiException<string>
    {
        public UserNotFoundApiException(string message) : base(message)
        {
        }

        public override int StatusCode => StatusCodes.Status400BadRequest;
    }
}<file_sep>﻿using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Tasks.Api.Data.Interfaces;
using Tasks.Api.Entities;

namespace Tasks.Api.Data.Repositories
{
    public class RoomTaskRepository : Repository<RoomTask>, IRoomTaskRepository
    {
        public RoomTaskRepository(ApplicationContext context) : base(context)
        {
        }

        public async Task<IEnumerable<RoomTask>> FindTasksInRoom(Guid roomId)
        {
            var room = await _context.Rooms.Include(x => x.RoomTasks).FirstOrDefaultAsync(r => r.RoomId == roomId);
            return room?.RoomTasks ?? new List<RoomTask>();
        }
    }
}<file_sep>using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using IdentityServer4.AccessTokenValidation;
using MassTransit;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Users.Api.Data;
using Users.Api.Services;
using Users.Data.Entities;

namespace Users.Api
{
    public class Startup
    {
        private readonly IConfiguration _configuration;

        public Startup(IConfiguration configuration) => _configuration = configuration;

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationContext>(options =>
                {
                    options.UseNpgsql(_configuration.GetConnectionString("Identity") + _configuration["DbName"]);
                })
                .AddIdentity<User, Role>(config =>
                {
                    config.User.RequireUniqueEmail = true;
                    config.Password.RequiredLength = 6;
                    config.Password.RequireDigit = true;
                    config.Password.RequireUppercase = false;
                })
                .AddEntityFrameworkStores<ApplicationContext>()
                .AddDefaultTokenProviders();

            services.AddSwaggerGen(options =>
            {
                options.EnableAnnotations();

                options.SwaggerDoc("v1", new OpenApiInfo
                {
                    Title = "UsersApi",
                    Version = "v1",
                    Description = "Service for register users, email confirm and update profile information",

                    Contact = new OpenApiContact
                    {
                        Email = "<EMAIL>",
                        Name = "<NAME>"
                    }
                });

                options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.OAuth2,

                    Flows = new OpenApiOAuthFlows
                    {
                        Password = new OpenApiOAuthFlow
                        {
                            TokenUrl = new Uri(_configuration["IdentityServer:TokenUrl"])
                        }
                    }
                });

                options.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "oauth2"
                            },
                            Scheme = "oauth2",
                            Name = "Bearer",
                            In = ParameterLocation.Header
                        },
                        new List<string>()
                    }
                });

                string xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
                string filePath = Path.Combine(AppContext.BaseDirectory, xmlFile);
                options.IncludeXmlComments(filePath);
            });

            services.AddAuthentication(options =>
                {
                    options.DefaultScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultAuthenticateScheme = IdentityServerAuthenticationDefaults.AuthenticationScheme;
                })
                .AddIdentityServerAuthentication(options =>
                {
                    options.Authority = _configuration["IdentityServer:Authority"];
                    options.RequireHttpsMetadata = false;
                });

            services.AddAutoMapper(Assembly.GetExecutingAssembly());

            services.AddTransient<DatabaseInitializer>();

            services.AddScoped<UserService>();
            services.AddSingleton<EmailService>();

            services.AddControllers();

            services.AddMassTransit(x =>
                x.UsingRabbitMq((_, configurator) =>
                {
                    configurator.Host(_configuration["MassTransit:Host"],
                        _configuration["MassTransit:VirtualHost"], options =>
                        {
                            options.Username(_configuration["MassTransit:Username"]);
                            options.Password(_configuration["MassTransit:Password"]);
                        });
                }));

            services.AddMassTransitHostedService();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DatabaseInitializer initializer)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            initializer.Initialize();

            app.UseSwagger();
            app.UseSwaggerUI(options =>
            {
                options.RoutePrefix = "";
                options.SwaggerEndpoint("/swagger/v1/swagger.json", "TasksApi");

                options.DocumentTitle = "TasksApi";

                options.OAuthClientId(_configuration["IdentityServer:ClientId"]);
                options.OAuthClientSecret(_configuration["IdentityServer:Secret"]);
            });

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseMiddleware<ExceptionMiddleware>();
            app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
        }
    }
}<file_sep>﻿using System;
using System.Collections.Generic;

namespace Tasks.Api.Entities
{
    public class RoomRole
    {
        public Guid RoomRoleId { get; set; }

        public string RoomRoleName { get; set; }

        public string? RoomRoleDescription { get; set; }

        public List<UserInTheRoom> UserInRooms { get; set; }
    }
}<file_sep>﻿using System;
using Microsoft.EntityFrameworkCore.Migrations;

namespace Tasks.Api.Migrations
{
    public partial class AddedRegisteredUsersTable : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropForeignKey(
                name: "FK_RoomTasks_UsersInRoom_TaskCreatorRoomId_TaskCreatorUserId",
                table: "RoomTasks");

            migrationBuilder.DropTable(
                name: "UsersInRoom");

            migrationBuilder.CreateTable(
                name: "Users",
                columns: table => new
                {
                    UserId = table.Column<Guid>(type: "uuid", nullable: false),
                    LastName = table.Column<string>(type: "text", nullable: false),
                    FirstName = table.Column<string>(type: "text", nullable: false),
                    Patronymic = table.Column<string>(type: "text", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Users", x => x.UserId);
                });

            migrationBuilder.CreateTable(
                name: "UserInTheRoom",
                columns: table => new
                {
                    RoomId = table.Column<Guid>(type: "uuid", nullable: false),
                    UserId = table.Column<Guid>(type: "uuid", nullable: false),
                    RoomRoleId = table.Column<Guid>(type: "uuid", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_UserInTheRoom", x => new { x.RoomId, x.UserId });
                    table.ForeignKey(
                        name: "FK_UserInTheRoom_RoomRoles_RoomRoleId",
                        column: x => x.RoomRoleId,
                        principalTable: "RoomRoles",
                        principalColumn: "RoomRoleId",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_UserInTheRoom_Rooms_RoomId",
                        column: x => x.RoomId,
                        principalTable: "Rooms",
                        principalColumn: "RoomId",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_UserInTheRoom_Users_UserId",
                        column: x => x.UserId,
                        principalTable: "Users",
                        principalColumn: "UserId",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                name: "IX_UserInTheRoom_RoomRoleId",
                table: "UserInTheRoom",
                column: "RoomRoleId");

            migrationBuilder.CreateIndex(
                name: "IX_UserInTheRoom_UserId",
                table: "UserInTheRoom",
                column: "UserId");

            migrationBuilder.AddForeignKey(
                name: "FK_RoomTasks_UserInTheRoom_TaskCreatorRoomId_TaskCreatorUserId",
                table: "RoomTasks",
                columns: new[] { "TaskCreatorRoomId", "TaskCreatorUserId" },
                principalTable: "UserInTheRoom",
                principalColumns: new[] { "RoomId", "UserId" },
                onDelete: ReferentialAction.Restrict);
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropForeignKey(
                name: "FK_RoomTasks_UserInTheRoom_TaskCreatorRoomId_TaskCreatorUserId",
                table: "RoomTasks");

            migrationBuilder.DropTable(
                name: "UserInTheRoom");

            migrationBuilder.DropTable(
                name: "Users");

            migrationBuilder.CreateTable(
                name: "UsersInRoom",
                columns: table => new
                {
                    RoomId = table.Column<Guid>(type: "uuid", nullable: false),
                    UserId = table.Column<Guid>(type: "uuid", nullable: false),
                    RoomRoleId = table.Column<Guid>(type: "uuid", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_UsersInRoom", x => new { x.RoomId, x.UserId });
                    table.ForeignKey(
                        name: "FK_UsersInRoom_RoomRoles_RoomRoleId",
                        column: x => x.RoomRoleId,
                        principalTable: "RoomRoles",
                        principalColumn: "RoomRoleId",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_UsersInRoom_Rooms_RoomId",
                        column: x => x.RoomId,
                        principalTable: "Rooms",
                        principalColumn: "RoomId",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                name: "IX_UsersInRoom_RoomRoleId",
                table: "UsersInRoom",
                column: "RoomRoleId");

            migrationBuilder.AddForeignKey(
                name: "FK_RoomTasks_UsersInRoom_TaskCreatorRoomId_TaskCreatorUserId",
                table: "RoomTasks",
                columns: new[] { "TaskCreatorRoomId", "TaskCreatorUserId" },
                principalTable: "UsersInRoom",
                principalColumns: new[] { "RoomId", "UserId" },
                onDelete: ReferentialAction.Restrict);
        }
    }
}
<file_sep>﻿using System.Collections.Generic;
using IdentityModel;
using IdentityServer4;
using IdentityServer4.Models;
using Microsoft.Extensions.Configuration;

namespace IdentityServer
{
    public static class Config
    {
        public static IEnumerable<ApiScope> ApiScopes =>
            new List<ApiScope>
            {
                new("TaskAPI"),
                new("AccountAPI"),
                new("IdentityServer"),
            };

        public static IEnumerable<Client> GetConfiguredClients(IConfiguration configuration) =>
            new List<Client>
            {
                new()
                {
                    ClientId = "SwaggerApp",
                    ClientSecrets =
                    {
                        new Secret(configuration["Clients:SwaggerApp:Secret"].ToSha256())
                    },
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    AllowedCorsOrigins =
                    {
                        configuration["Clients:SwaggerApp:Origin1"],
                        configuration["Clients:SwaggerApp:Origin2"]
                    },
                    RequireClientSecret = true,
                    AllowedScopes =
                    {
                        "IdentityServer",
                        IdentityServerConstants.StandardScopes.OpenId,
                    }
                },
                new()
                {
                    ClientId = "Gateway",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    AllowedCorsOrigins =
                    {
                        configuration["Clients:Gateway:Origin"]
                    },
                    RequireClientSecret = true,
                    ClientSecrets =
                    {
                        new Secret(configuration["Clients:Gateway:Secret"].ToSha256())
                    },
                    AllowedScopes =
                    {
                        "IdentityServer",
                        IdentityServerConstants.StandardScopes.OpenId,
                    }
                },
                new()
                {
                    ClientId = "MobileApp",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    RequireClientSecret = false,
                    AllowedScopes =
                    {
                        "TaskAPI",
                        "FullName",
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Email,
                        IdentityServerConstants.StandardScopes.Phone
                    }
                },
            };

        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.Email(),
                new IdentityResources.OpenId(),
                new IdentityResources.Phone(),
                new("FullName", new List<string>
                {
                    "full_name"
                })
            };
        }
    }
}<file_sep>using Microsoft.AspNetCore.Http;

namespace Tasks.Api.Exceptions
{
    public class AlreadyDoneApiException : ApiException
    {
        public AlreadyDoneApiException(string message) : base(message)
        {
        }

        public override int StatusCode => StatusCodes.Status400BadRequest;
    }
}<file_sep>﻿using System.Linq;
using Microsoft.EntityFrameworkCore;
using Tasks.Api.Entities;

namespace Tasks.Api.Data
{
    public class DatabaseInitializer
    {
        private readonly ApplicationContext _database;

        public DatabaseInitializer(ApplicationContext database) => _database = database;

        public void Initialize()
        {
            _database.Database.Migrate();
            EnsureRolesAdded();
            _database.SaveChanges();
        }

        private void EnsureRolesAdded()
        {
            if (!_database.RoomRoles.Any())
            {
                _database.RoomRoles.AddRange(Roles.AllRoles().Select(name => new RoomRole
                {
                    RoomRoleName = name
                }));
            }
        }
    }
}<file_sep>﻿using System;

namespace Tasks.Api.Entities
{
    public class RoomTask
    {
        public Guid RoomTaskId { get; set; }

        public string TaskName { get; set; }

        public string TaskContent { get; set; }

        public string? Details { get; set; }

        public bool IsCompleted { get; set; }

        public Guid RoomId { get; set; }

        public Room Room { get; set; }

        public DateTime TaskCreationTime { get; set; } = DateTime.Now;

        public Guid TaskCreatorId { get; set; }

        public User TaskCreator { get; set; }

        public DateTime DeadlineTime { get; set; }
    }
}<file_sep>using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using Tasks.Api.Services;
using Tasks.Api.ViewModels.RoleViewModels;

namespace Tasks.Api.Controllers
{
    /// <summary>
    ///     Everything about roles
    /// </summary>
    [ApiController]
    [Route("api/")]
    [SwaggerTag("Everything about roles")]
    public class RolesController : ControllerBase
    {
        private readonly RoleService _roleService;

        public RolesController(RoleService roleService) => _roleService = roleService;

        /// <summary>
        ///     Finds all roles in rooms
        /// </summary>
        /// <returns></returns>
        [HttpGet("[controller]")]
        [SwaggerResponse(StatusCodes.Status200OK, type: typeof(List<RoleViewModel>))]
        public async Task<ActionResult<List<string>>> AllRoles() => Ok(await _roleService.AllRoles());

        /// <summary>
        ///     Updates user role in room
        /// </summary>
        /// <param name="roomId"></param>
        /// <param name="viewModel"></param>
        /// <returns></returns>
        [Authorize]
        [HttpPut("rooms/{roomId:guid}/[controller]")]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status401Unauthorized, "If user is unauthorized")]
        [SwaggerResponse(StatusCodes.Status404NotFound, "If id is incorrect", typeof(ProblemDetails))]
        [SwaggerResponse(StatusCodes.Status403Forbidden, "If user has insufficient rights", typeof(ProblemDetails))]
        public async Task<ActionResult> UpdateRoleForUser(Guid roomId, UpdateUserRoleViewModel viewModel)
        {
            await _roleService.UpdateRoleForUser(roomId, viewModel, UserService.GetCurrentUserId(HttpContext));
            return Ok();
        }

        /// <summary>
        ///     Returns information about user role in room
        /// </summary>
        /// <param name="roomId"></param>
        /// <returns></returns>
        [Authorize]
        [HttpGet("rooms/{roomId:guid}/[controller]")]
        [SwaggerResponse(StatusCodes.Status200OK, type: typeof(List<UserWithRoleViewModel>))]
        [SwaggerResponse(StatusCodes.Status401Unauthorized, "If user is unauthorized")]
        [SwaggerResponse(StatusCodes.Status404NotFound, "If id is incorrect", typeof(ProblemDetails))]
        [SwaggerResponse(StatusCodes.Status403Forbidden, "If user has insufficient rights", typeof(ProblemDetails))]
        public async Task<ActionResult<List<UserWithRoleViewModel>>> UsersInRoomWIthRoles(Guid roomId)
        {
            var result = await _roleService.GetUsersInRoomWithRoles(roomId, UserService.GetCurrentUserId(HttpContext));
            return Ok(result);
        }
    }
}<file_sep>using AutoMapper;
using Tasks.Api.Entities;
using Tasks.Api.ViewModels.RoleViewModels;
using Tasks.Api.ViewModels.UserViewModel;
using Users.Data;

namespace Tasks.Api.Profiles
{
    public class UserProfile : Profile
    {
        public UserProfile()
        {
            CreateMap<UserInTheRoom, UpdateUserRoleViewModel>();

            CreateMap<UserRegistered, User>();

            CreateMap<User, UserFullNameViewModel>();

            CreateMap<ProfileUpdated, User>();
        }
    }
}<file_sep>﻿using System;
using Microsoft.EntityFrameworkCore.Migrations;

namespace Tasks.Api.Migrations
{
    public partial class AddedTasks : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "RoomTasks",
                columns: table => new
                {
                    RoomTaskId = table.Column<Guid>(type: "uuid", nullable: false),
                    TaskName = table.Column<string>(type: "text", nullable: false),
                    TaskContent = table.Column<string>(type: "text", nullable: false),
                    Details = table.Column<string>(type: "text", nullable: false),
                    RoomId = table.Column<Guid>(type: "uuid", nullable: false),
                    TaskCreationTime = table.Column<DateTime>(type: "timestamp without time zone", nullable: false),
                    TaskCreatorId = table.Column<Guid>(type: "uuid", nullable: false),
                    TaskCreatorRoomId = table.Column<Guid>(type: "uuid", nullable: true),
                    TaskCreatorUserId = table.Column<Guid>(type: "uuid", nullable: true),
                    DeadlineTime = table.Column<DateTime>(type: "timestamp without time zone", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_RoomTasks", x => x.RoomTaskId);
                    table.ForeignKey(
                        name: "FK_RoomTasks_Rooms_RoomId",
                        column: x => x.RoomId,
                        principalTable: "Rooms",
                        principalColumn: "RoomId",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_RoomTasks_UsersInRoom_TaskCreatorRoomId_TaskCreatorUserId",
                        columns: x => new { x.TaskCreatorRoomId, x.TaskCreatorUserId },
                        principalTable: "UsersInRoom",
                        principalColumns: new[] { "RoomId", "UserId" },
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateIndex(
                name: "IX_RoomTasks_RoomId",
                table: "RoomTasks",
                column: "RoomId");

            migrationBuilder.CreateIndex(
                name: "IX_RoomTasks_TaskCreatorRoomId_TaskCreatorUserId",
                table: "RoomTasks",
                columns: new[] { "TaskCreatorRoomId", "TaskCreatorUserId" });
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "RoomTasks");
        }
    }
}
<file_sep>using System.ComponentModel.DataAnnotations;

namespace Users.Api.ViewModels
{
    public class UpdateProfileViewModel
    {
        [Required]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [StringLength(50)]
        public string FirstName { get; set; }

        [Required]
        [StringLength(50)]
        public string Patronymic { get; set; }

        [Phone]
        public string PhoneNumber { get; set; }
    }
}<file_sep>using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AutoMapper;
using MassTransit;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Users.Api.Exceptions;
using Users.Api.ViewModels;
using Users.Data;
using Users.Data.Entities;

namespace Users.Api.Services
{
    public class UserService
    {
        private readonly IConfiguration _configuration;

        private readonly EmailService _emailService;

        private readonly IWebHostEnvironment _environment;

        private readonly IMapper _mapper;

        private readonly IPublishEndpoint _publishEndpoint;

        private readonly UserManager<User> _userManager;

        public UserService(IWebHostEnvironment environment, IPublishEndpoint publishEndpoint,
            IConfiguration configuration, UserManager<User> userManager, EmailService emailService, IMapper mapper)
        {
            _environment = environment;
            _publishEndpoint = publishEndpoint;
            _configuration = configuration;
            _userManager = userManager;
            _emailService = emailService;
            _mapper = mapper;
        }

        public async Task Register(RegisterViewModel viewModel)
        {
            var user = _mapper.Map<User>(viewModel);
            var result = await _userManager.CreateAsync(user, viewModel.Password);

            if (result.Succeeded)
            {
                await _publishEndpoint.Publish(_mapper.Map<UserRegistered>(user));
                try
                {
                    await SendConfirmationMail(viewModel.Email);
                }
                catch (Exception)
                {
                    // ignored
                }

                return;
            }

            var validationProblemDetails = new ValidationProblemDetails();
            List<string> passwordErrors = new();
            List<string> emailErrors = new();
            List<string> otherErrors = new();

            result.Errors.Select(x => x.Description).ToList().ForEach(x =>
            {
                if (x.Contains("password", StringComparison.InvariantCultureIgnoreCase))
                    passwordErrors.Add(x);
                else if (x.Contains("email", StringComparison.InvariantCultureIgnoreCase))
                    emailErrors.Add(x);
                else
                    otherErrors.Add(x);
            });

            AddErrorsForKeyIfNotEmpty(validationProblemDetails, "Email", emailErrors);
            AddErrorsForKeyIfNotEmpty(validationProblemDetails, "Password", passwordErrors);
            AddErrorsForKeyIfNotEmpty(validationProblemDetails, "Other", otherErrors);

            throw new RegistrationException(validationProblemDetails);
        }

        public async Task SendConfirmationMail(string email)
        {
            var createdUser = await _userManager.FindByEmailAsync(email);
            string token =
                Uri.EscapeDataString(await _userManager.GenerateEmailConfirmationTokenAsync(createdUser));
            string callbackUrl =
                $"{_configuration["Gateway:Host"]}/{_configuration["Gateway:ConfirmMailRoute"]}/{createdUser.Id}/{token}";

            await _emailService.SendEmailAsync(email, "Email confirmation",
                string.Format(await File.ReadAllTextAsync(Path.Combine(_environment.ContentRootPath, "mail.html")),
                    callbackUrl));
        }

        public async Task UpdateProfileAsync(Guid userId, UpdateProfileViewModel viewModel)
        {
            var user = await _userManager.FindByIdAsync(userId.ToString());

            if (user == null)
                throw new UserNotFoundApiException("Token is invalid: user with given id was not found!");

            var updatedProfile = _mapper.Map<ProfileUpdated>(viewModel);
            updatedProfile.UserId = user.Id;
            await _publishEndpoint.Publish(updatedProfile);

            user = _mapper.Map(viewModel, user);
            await _userManager.UpdateAsync(user);
        }

        public static Guid GetCurrentUserId(HttpContext context) =>
            Guid.Parse(context.User.FindFirstValue("sub") ?? string.Empty);

        private static void AddErrorsForKeyIfNotEmpty(ValidationProblemDetails details, string key, List<string> errors)
        {
            if (!errors.Any())
                return;
            details.Errors[key] = errors.ToArray();
        }
    }
}<file_sep>using System;
using System.Collections.Generic;

namespace Tasks.Api.Entities
{
    public class User
    {
        public Guid UserId { get; set; }

        public string LastName { get; set; }

        public string FirstName { get; set; }

        public string Patronymic { get; set; }

        public List<UserInTheRoom> UserInTheRooms { get; set; }
    }
}<file_sep>using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Tasks.Api.Entities;

namespace Tasks.Api.Data.Interfaces
{
    public interface IRoomRepository : IRepository<Room>
    {
        Task<UserInTheRoom?> FindUserInRoom(Guid roomId, Guid userId);

        Task<Room?> FindRoomWithUserInRole(Guid roomId, Guid userId, string roleName);

        Task<IEnumerable<Room>> FindRoomsForUser(Guid userId);

        Task<Room?> FindRoomWithUsers(Guid roomId);
    }
}<file_sep>﻿using System;
using Microsoft.AspNetCore.Http;

namespace Tasks.Api.Exceptions
{
    [Serializable]
    public class NotFoundApiException : ApiException
    {
        public NotFoundApiException(string message) : base(message)
        {
        }

        public override int StatusCode => StatusCodes.Status404NotFound;
    }
}<file_sep>using AutoMapper;
using Users.Api.ViewModels;
using Users.Data;
using Users.Data.Entities;

namespace Users.Api.Profiles
{
    public class UserProfile : Profile
    {
        public UserProfile()
        {
            CreateMap<RegisterViewModel, User>()
                .ForMember(dst => dst.UserName, options => options.MapFrom(src => src.Email));

            CreateMap<UpdateProfileViewModel, User>();

            CreateMap<User, UserRegistered>().ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.Id));

            CreateMap<UpdateProfileViewModel, ProfileUpdated>();
        }
    }
}<file_sep>﻿using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Tasks.Api.Data.Interfaces;
using Tasks.Api.Entities;

namespace Tasks.Api.Data.Repositories
{
    public class RoomRoleRepository : Repository<RoomRole>, IRoomRoleRepository
    {
        public RoomRoleRepository(ApplicationContext context) : base(context)
        {
        }

        public async Task<IEnumerable<RoomRole>> FindAllRoles() =>
            await _context.RoomRoles.ToListAsync();

        public async Task<RoomRole> FindWithName(string roleName) =>
            await _context.RoomRoles.FirstAsync(x => x.RoomRoleName == roleName);
    }
}<file_sep>using System.Threading.Tasks;
using AutoMapper;
using MassTransit;
using Tasks.Api.Data;
using Tasks.Api.Entities;
using Users.Data;

namespace Tasks.Api.MassTransit
{
    public class UserRegisteredConsumer : IConsumer<UserRegistered>
    {
        private readonly ApplicationContext _applicationContext;

        private readonly IMapper _mapper;

        public UserRegisteredConsumer(IMapper mapper, ApplicationContext applicationContext)
        {
            _mapper = mapper;
            _applicationContext = applicationContext;
        }

        public async Task Consume(ConsumeContext<UserRegistered> context)
        {
            await _applicationContext.Users.AddAsync(_mapper.Map<User>(context.Message));
            await _applicationContext.SaveChangesAsync();
        }
    }
}<file_sep>using System;

namespace Tasks.Api.ViewModels.UserViewModel
{
    public class UserFullNameViewModel
    {
        public Guid UserId { get; set; }

        public string LastName { get; set; }

        public string FirstName { get; set; }

        public string Patronymic { get; set; }
    }
}<file_sep>﻿using AutoMapper;
using Tasks.Api.Entities;
using Tasks.Api.ViewModels.TaskViewModels;

namespace Tasks.Api.Profiles
{
    public class TaskProfile : Profile
    {
        public TaskProfile()
        {
            CreateMap<AddTaskViewModel, RoomTask>()
                .ForMember(dest => dest.RoomId, cfg => cfg.Ignore());

            CreateMap<RoomTask, TaskViewModel>();
        }
    }
}<file_sep>﻿using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using Tasks.Api.Data;
using Tasks.Api.Entities;
using Tasks.Api.Exceptions;
using Tasks.Api.ViewModels.TaskViewModels;
using Tasks.Api.ViewModels.UserViewModel;

namespace Tasks.Api.Services
{
    public class RoomTaskService
    {
        private readonly IMapper _mapper;

        private readonly UnitOfWork _unitOfWork;

        private readonly UserService _userService;

        public RoomTaskService(IMapper mapper, UnitOfWork unitOfWork, UserService userService)
        {
            _mapper = mapper;
            _unitOfWork = unitOfWork;
            _userService = userService;
        }

        public async Task<IEnumerable<TaskViewModel>> FindAllTasksInRoom(Guid roomId)
        {
            var tasks = await _unitOfWork.RoomTaskRepository.FindTasksInRoom(roomId);
            return _mapper.Map<IEnumerable<TaskViewModel>>(tasks);
        }

        public async Task<TaskViewModel?> FindTask(Guid roomId, Guid taskId, Guid userId)
        {
            var task = await _unitOfWork.RoomTaskRepository.Find(x => x.RoomTaskId == taskId);
            if (task?.RoomId != roomId)
                throw new NotFoundApiException(AppExceptions.TaskInRoomNotFoundException);

            await _userService.ThrowIfNotRoomMember(roomId, userId);

            return _mapper.Map<TaskViewModel>(task);
        }

        public async Task<TaskViewModel?> CompleteTask(Guid roomId, Guid taskId, Guid userId)
        {
            var task = await _unitOfWork.RoomTaskRepository.Find(x => x.RoomTaskId == taskId);
            if (task?.RoomId != roomId)
                throw new NotFoundApiException(AppExceptions.TaskInRoomNotFoundException);
            await _userService.ThrowIfNotRoomMember(roomId, userId);

            task.IsCompleted = true;
            await _unitOfWork.SaveChangesAsync();
            return _mapper.Map<TaskViewModel>(task);
        }

        public async Task<TaskViewModel> CreateTask(Guid roomId, AddTaskViewModel viewModel, Guid userId)
        {
            var room = await _unitOfWork.RoomRepository.Find(r => r.RoomId == roomId);

            if (room == null)
                throw new NotFoundApiException(AppExceptions.RoomNotFoundException);

            await _userService.ThrowIfNotRoomMember(roomId, userId);

            if (!await _userService.CheckUserHasAnyRole(roomId, userId, Roles.Owner, Roles.Administrator))
                throw new AccessRightApiException(AppExceptions.CreatorOrAdministratorOnlyCanDoThisException);

            var task = _mapper.Map<RoomTask>(viewModel);
            task.TaskCreatorId = userId;
            task.Room = room;

            var addedTask = await _unitOfWork.RoomTaskRepository.Create(task);
            await _unitOfWork.SaveChangesAsync();

            return _mapper.Map<TaskViewModel>(addedTask);
        }

        public async Task UpdateTask(Guid roomId, Guid taskId, AddTaskViewModel viewModel, Guid userId)
        {
            var task = await _unitOfWork.RoomTaskRepository.Find(x => x.RoomTaskId == taskId);

            if (task == null)
                throw new NotFoundApiException(AppExceptions.TaskNotFoundException);

            ThrowIfTaskNotInRoom(task, roomId);
            await _userService.ThrowIfNotRoomMember(roomId, userId);

            if (!await _userService.CheckUserHasAnyRole(task.RoomId, userId, Roles.Owner, Roles.Administrator))
                throw new AccessRightApiException(AppExceptions.CreatorOrAdministratorOnlyCanDoThisException);

            task = _mapper.Map(viewModel, task);
            _unitOfWork.RoomTaskRepository.Update(task);

            await _unitOfWork.SaveChangesAsync();
        }

        public async Task DeleteTask(Guid roomId, Guid taskId, Guid userId)
        {
            var task = await _unitOfWork.RoomTaskRepository.Find(x => x.RoomTaskId == taskId);

            if (task == null)
                throw new NotFoundApiException(AppExceptions.TaskNotFoundException);

            ThrowIfTaskNotInRoom(task, roomId);
            await _userService.ThrowIfNotRoomMember(roomId, userId);

            if (!await _userService.CheckUserHasAnyRole(task.RoomId, userId, Roles.Owner, Roles.Administrator))
                throw new AccessRightApiException(AppExceptions.CreatorOrAdministratorOnlyCanDoThisException);

            _unitOfWork.RoomTaskRepository.Delete(task);

            await _unitOfWork.SaveChangesAsync();
        }

        private static void ThrowIfTaskNotInRoom(RoomTask roomTask, Guid roomId)
        {
            if (roomTask.RoomId != roomId)
                throw new NotFoundApiException(AppExceptions.TaskInRoomNotFoundException);
        }

        public async Task<UserFullNameViewModel> FindTaskCreator(Guid roomId, Guid taskId, Guid userId)
        {
            var task = await _unitOfWork.RoomTaskRepository.Find(x => x.RoomTaskId == taskId);

            if (task == null)
                throw new NotFoundApiException(AppExceptions.TaskNotFoundException);

            ThrowIfTaskNotInRoom(task, roomId);
            await _userService.ThrowIfNotRoomMember(roomId, userId);

            var userInRoom = await _unitOfWork.RoomRepository.FindUserInRoom(roomId, task.TaskCreatorId);
            if (userInRoom == null)
            {
                throw new NotFoundApiException(AppExceptions.CreatorNotFound);
            }

            return _mapper.Map<UserFullNameViewModel>(userInRoom.User);
        }
    }
}<file_sep>﻿using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using Users.Api.Services;
using Users.Api.ViewModels;
using Users.Data.Entities;

namespace Users.Api.Controllers
{
    /// <summary>
    ///     Operations about users
    /// </summary>
    [ApiController]
    [Route("api/[controller]")]
    [SwaggerTag("Operations about users")]
    public class AccountsController : ControllerBase
    {
        private readonly UserManager<User> _userManager;

        private readonly UserService _userService;

        /// <inheritdoc />
        public AccountsController(UserService userService, UserManager<User> userManager)
        {
            _userService = userService;
            _userManager = userManager;
        }

        /// <summary>
        ///     Registers a new user
        /// </summary>
        /// <param name="viewModel"></param>
        /// <returns></returns>
        [HttpPost("[action]")]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status400BadRequest, "If data is invalid", typeof(ValidationProblemDetails))]
        public async Task<ActionResult> RegisterAsync(RegisterViewModel viewModel)
        {
            await _userService.Register(viewModel);
            return Ok();
        }

        /// <summary>
        ///     Confirms email for user
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="token"></param>
        /// <returns></returns>
        [HttpGet("confirm-email/{userId}/{token}")]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status404NotFound)]
        [SwaggerResponse(StatusCodes.Status400BadRequest)]
        public async Task<ActionResult> ConfirmEmailAsync(string userId, string token)
        {
            if (!Guid.TryParse(userId, out _))
                return BadRequest("User id is not guid");

            var user = await _userManager.FindByIdAsync(userId);
            if (user == null)
                return BadRequest("User id is invalid");

            var result = await _userManager.ConfirmEmailAsync(user, Uri.UnescapeDataString(token));
            if (!result.Succeeded)
                return BadRequest("Token is invalid");

            return Ok("Ok");
        }

        /// <summary>
        ///     Resends confirmation mail for registered email
        /// </summary>
        /// <param name="registeredEmail"></param>
        /// <returns></returns>
        [HttpGet("resend-confirmation-mail")]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status400BadRequest, "If email if not registered", typeof(ProblemDetails))]
        [SwaggerResponse(StatusCodes.Status409Conflict, "If email if already confirmed", typeof(ProblemDetails))]
        public async Task<ActionResult> ReSendEmailAsync([FromHeader] string registeredEmail)
        {
            var user = await _userManager.FindByEmailAsync(registeredEmail);
            if (user == null)
                return BadRequest(new ProblemDetails
                {
                    Detail = "Email is not registered!"
                });
            if (user.EmailConfirmed)
                return Conflict(new ProblemDetails
                {
                    Detail = "Already confirmed"
                });

            await _userService.SendConfirmationMail(registeredEmail);
            return Ok();
        }

        /// <summary>
        ///     Returns user profile information
        /// </summary>
        /// <param name="viewModel"></param>
        /// <returns></returns>
        [Authorize]
        [HttpPut("[action]")]
        [SwaggerResponse(StatusCodes.Status200OK)]
        [SwaggerResponse(StatusCodes.Status400BadRequest, "If token is invalid", typeof(ProblemDetails))]
        public async Task<ActionResult> Profile(UpdateProfileViewModel viewModel)
        {
            await _userService.UpdateProfileAsync(UserService.GetCurrentUserId(HttpContext), viewModel);
            return Ok();
        }
    }
}<file_sep># Work planner Backend Web API

Backend part for my project "Work planner"

See also [frontend part](https://github.com/Inozpavel/WorkPlanner.Android)

The main idea of the project is a work execution planner. Users can create rooms and tasks within them. They can then
invite other users to join their rooms or join existing room then issue tasks and view their statuses.

The security system is made using identity server 4. There are registration, authorization systems. After registration,
the user will be sent an email for confirmation. Users have roles inside the rooms.

It is microservice architecture project. All access to microservices is implemented through the api gateway. Also, each
microservice is located in a docker container.

| Status | Master | Develop |
| ------ | ------ | ------- |
|  Build | [![CI](https://github.com/Inozpavel/WorkPlanner.WebApi/actions/workflows/dotnet.yml/badge.svg?branch=master&event=push)](https://github.com/Inozpavel/WorkPlanner.WebApi/actions/workflows/dotnet.yml) | [![CI](https://github.com/Inozpavel/WorkPlanner.WebApi/actions/workflows/dotnet.yml/badge.svg?branch=dev&event=push)](https://github.com/Inozpavel/WorkPlanner.WebApi/actions/workflows/dotnet.yml) |

## Prerequisites for launching

[Docker Desktop](https://www.docker.com/products/docker-desktop)

## Run system locally using Docker images

To run project open console in folder with [docker-compose.yml](docker-compose.yml?raw=true) file and run this commands

1. Pull services images from Docker Hub

```cmd
docker-compose pull
```

2. Run services

```cmd
docker-compose up -d
```

After starting API will be available on [localhost:4000](http://localhost:4000)

Documentation will be available on [localhost:4000/swagger](http://localhost:4000/swagger)

## Project Information

### Project technologies stack

Main framework

- [ASP.Net Core](https://dotnet.microsoft.com/apps/aspnet)

Database

- [PostgreSQL](https://www.postgresql.org/)

Security

- [Identity Server 4](https://identityserver4.readthedocs.io/en/latest/)

ORM

- [Entity Framework Core for PostgreSQL](https://www.nuget.org/packages?q=postgre)

API Gateway

- [Ocelot](https://github.com/ThreeMammals/Ocelot)

Message broker

- [RabbitMQ (MassTransit)](https://masstransit-project.com/usage/transports/rabbitmq.html)

Documentation

- [Swagger](https://swagger.io/)

Packages

- [AutoMapper](https://github.com/AutoMapper/AutoMapper)
- [AspNetCore.Identity](https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.EntityFrameworkCore/)

## Project Architecture

![Architecture](Architecture.png)

Patterns

- UnitOfWork
- Repository
- Microservices architecture<file_sep>﻿using System;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Users.Data.Entities;

namespace IdentityServer.Data
{
    public class ApplicationContext : IdentityDbContext<User, Role, Guid>
    {
        public ApplicationContext(DbContextOptions options) : base(options)
        {
        }
    }
}<file_sep>﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Tasks.Api.Data.Interfaces;
using Tasks.Api.Entities;

namespace Tasks.Api.Data.Repositories
{
    public class RoomRepository : Repository<Room>, IRoomRepository
    {
        public RoomRepository(ApplicationContext context) : base(context)
        {
        }

        public async Task<UserInTheRoom?> FindUserInRoom(Guid roomId, Guid userId)
        {
            return await _context.UsersInRoom.Include(x => x.RoomRole)
                .Include(x => x.User)
                .FirstOrDefaultAsync(x => x.RoomId == roomId && x.UserId == userId);
        }

        public async Task<Room?> FindRoomWithUserInRole(Guid roomId, Guid userId, string roleName)
        {
            return await _context.Rooms.FirstOrDefaultAsync(x =>
                x.RoomId == roomId &&
                x.UsersInRoom.Any(u => u.UserId == userId && u.RoomRole.RoomRoleName == roleName));
        }

        public async Task<IEnumerable<Room>> FindRoomsForUser(Guid userId) =>
            await _context.Rooms.Where(x => x.UsersInRoom.Any(u => u.UserId == userId)).ToListAsync();

        public async Task<Room?> FindRoomWithUsers(Guid roomId) =>
            await _context.Rooms.Include(x => x.UsersInRoom).ThenInclude(x => x.User)
                .FirstOrDefaultAsync(x => x.RoomId == roomId);
    }
}<file_sep>using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Users.Api.Exceptions
{
    public class RegistrationException : ApiException<ValidationProblemDetails>
    {
        public RegistrationException(ValidationProblemDetails problemDetails) : base(problemDetails)
        {
        }

        public override int StatusCode => StatusCodes.Status400BadRequest;
    }
}<file_sep>﻿using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Users.Api.Exceptions;

namespace Users.Api
{
    public class ExceptionMiddleware
    {
        private readonly RequestDelegate _next;

        public ExceptionMiddleware(RequestDelegate next) => _next = next;

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await _next(context);
            }
            catch (RegistrationException e)
            {
                context.Response.StatusCode = e.StatusCode;

                await context.Response.WriteAsJsonAsync(e.ErrorData);
            }
            catch (ApiException e)
            {
                context.Response.StatusCode = e.StatusCode;

                await context.Response.WriteAsJsonAsync(new ProblemDetails
                {
                    Detail = e.Message,
                    Status = e.StatusCode,
                });
            }
        }
    }
}<file_sep>using System.Net;
using System.Net.Mail;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;

namespace Users.Api.Services
{
    public class EmailService
    {
        private readonly IConfiguration _configuration;

        public EmailService(IConfiguration configuration) => _configuration = configuration;

        public async Task SendEmailAsync(string email, string subject, string body)
        {
            var emailMessage = new MailMessage
            {
                From = new MailAddress(_configuration["SmtpServer:Login"]),
                Subject = subject,
                Body = body,
                IsBodyHtml = true
            };
            emailMessage.To.Add(email);

            var client = new SmtpClient
            {
                Host = _configuration["SmtpServer:Host"],
                Port = _configuration.GetValue<int>("SmtpServer:Port"),
                EnableSsl = _configuration.GetValue<bool>("SmtpServer:EnableSsl"),
                Credentials = new NetworkCredential(_configuration["SmtpServer:Login"],
                    _configuration["SmtpServer:Password"])
            };

            await client.SendMailAsync(emailMessage);
        }
    }
}<file_sep>﻿using System;
using Microsoft.EntityFrameworkCore.Migrations;

namespace Tasks.Api.Migrations
{
    public partial class Initial : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                "RoomRoles",
                table => new
                {
                    RoomRoleId = table.Column<Guid>("uuid", nullable: false),
                    RoomRoleName = table.Column<string>("text", nullable: false),
                    RoomRoleDescription = table.Column<string>("text", nullable: true)
                },
                constraints: table => { table.PrimaryKey("PK_RoomRoles", x => x.RoomRoleId); });

            migrationBuilder.CreateTable(
                "Rooms",
                table => new
                {
                    RoomId = table.Column<Guid>("uuid", nullable: false),
                    OwnerId = table.Column<Guid>("uuid", nullable: false),
                    RoomName = table.Column<string>("text", nullable: false),
                    RoomDescription = table.Column<string>("text", nullable: true),
                    CreationDate = table.Column<DateTime>("timestamp without time zone", nullable: false)
                },
                constraints: table => { table.PrimaryKey("PK_Rooms", x => x.RoomId); });

            migrationBuilder.CreateTable(
                "UsersInRoom",
                table => new
                {
                    RoomId = table.Column<Guid>("uuid", nullable: false),
                    UserId = table.Column<Guid>("uuid", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_UsersInRoom", x => new {x.RoomId, x.UserId});
                    table.ForeignKey(
                        "FK_UsersInRoom_Rooms_RoomId",
                        x => x.RoomId,
                        "Rooms",
                        "RoomId",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateTable(
                "RoomRoleUserInRoom",
                table => new
                {
                    RoomRolesRoomRoleId = table.Column<Guid>("uuid", nullable: false),
                    UserInRoomsRoomId = table.Column<Guid>("uuid", nullable: false),
                    UserInRoomsUserId = table.Column<Guid>("uuid", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_RoomRoleUserInRoom",
                        x => new {x.RoomRolesRoomRoleId, x.UserInRoomsRoomId, x.UserInRoomsUserId});
                    table.ForeignKey(
                        "FK_RoomRoleUserInRoom_RoomRoles_RoomRolesRoomRoleId",
                        x => x.RoomRolesRoomRoleId,
                        "RoomRoles",
                        "RoomRoleId",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        "FK_RoomRoleUserInRoom_UsersInRoom_UserInRoomsRoomId_UserInRoom~",
                        x => new {x.UserInRoomsRoomId, x.UserInRoomsUserId},
                        "UsersInRoom",
                        new[] {"RoomId", "UserId"},
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                "IX_RoomRoleUserInRoom_UserInRoomsRoomId_UserInRoomsUserId",
                "RoomRoleUserInRoom",
                new[] {"UserInRoomsRoomId", "UserInRoomsUserId"});
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                "RoomRoleUserInRoom");

            migrationBuilder.DropTable(
                "RoomRoles");

            migrationBuilder.DropTable(
                "UsersInRoom");

            migrationBuilder.DropTable(
                "Rooms");
        }
    }
}<file_sep>using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace ApiGateway.Ocelot
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
                .ConfigureLogging(options => { options.AddConsole(); }).ConfigureAppConfiguration(
                    (hostingContext, config) =>
                    {
                        if (hostingContext.HostingEnvironment.IsDevelopment())
                            config.AddJsonFile("ocelot.json", false, true);
                        else if (hostingContext.HostingEnvironment.IsProduction())
                            config.AddJsonFile("ocelot.production.json", false, true);
                    });
    }
}<file_sep>using System;
using System.Collections.Generic;
using IdentityServer4.AccessTokenValidation;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Swashbuckle.AspNetCore.SwaggerUI;

namespace ApiGateway.Ocelot
{
    public class Startup
    {
        private readonly IConfiguration _configuration;

        public Startup(IConfiguration configuration) => _configuration = configuration;

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddOcelot();

            services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
                .AddIdentityServerAuthentication(
                    options =>
                    {
                        options.Authority = _configuration["IdentityServer:Authority"];
                        options.RequireHttpsMetadata = false;
                    });

            services.AddSwaggerForOcelot(_configuration, o =>
            {
                o.GenerateDocsDocsForGatewayItSelf(opt =>
                {
                    opt.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
                    {
                        Type = SecuritySchemeType.OAuth2,

                        Flows = new OpenApiOAuthFlows
                        {
                            Password = new <PASSWORD>
                            {
                                TokenUrl = new Uri(_configuration["IdentityServer:TokenUrl"])
                            }
                        }
                    });

                    opt.AddSecurityRequirement(new OpenApiSecurityRequirement
                    {
                        {
                            new OpenApiSecurityScheme
                            {
                                Reference = new OpenApiReference
                                {
                                    Type = ReferenceType.SecurityScheme,
                                    Id = "oauth2"
                                },
                                Scheme = "oauth2",
                                Name = "Bearer",
                                In = ParameterLocation.Header
                            },
                            new List<string>()
                        }
                    });
                });
            });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseSwaggerForOcelotUI(options =>
            {
                options.OAuthConfigObject = new OAuthConfigObject
                {
                    ClientId = _configuration["IdentityServer:ClientId"],
                    ClientSecret = _configuration["IdentityServer:Secret"]
                };
            });
            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/",
                    async context => await context.Response.WriteAsync("Here is ocelot!\n Try also /swagger"));
                endpoints.MapControllers();
            });

            app.UseOcelot().Wait();
        }
    }
}<file_sep>using System;

namespace Tasks.Api.ViewModels.RoleViewModels
{
    public class UpdateUserRoleViewModel
    {
        public Guid UserId { get; set; }

        public Guid RoomRoleId { get; set; }
    }
}<file_sep>﻿using AutoMapper;
using Tasks.Api.Entities;
using Tasks.Api.ViewModels.RoomViewModels;

namespace Tasks.Api.Profiles
{
    public class RoomProfile : Profile
    {
        public RoomProfile()
        {
            CreateMap<AddOrUpdateRoomViewModel, Room>();

            CreateMap<Room, RoomViewModel>();
        }
    }
}