diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 3445f6d79d..57fc1f1adb 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -6,8 +6,6 @@ using Bit.Core.Repositories; using Microsoft.AspNetCore.Authorization; using Bit.Api.Models; using Bit.Core.Exceptions; -using Bit.Core.Domains; -using Microsoft.AspNetCore.Identity; using Bit.Core.Services; namespace Bit.Api.Controllers diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs new file mode 100644 index 0000000000..4716a9a054 --- /dev/null +++ b/src/Api/Controllers/OrganizationsController.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Bit.Core.Repositories; +using Microsoft.AspNetCore.Authorization; +using Bit.Api.Models; +using Bit.Core.Exceptions; +using Bit.Core.Services; + +namespace Bit.Api.Controllers +{ + [Route("organizations")] + [Authorize("Application")] + public class OrganizationsController : Controller + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IUserService _userService; + + public OrganizationsController( + IOrganizationRepository organizationRepository, + IUserService userService) + { + _organizationRepository = organizationRepository; + _userService = userService; + } + + [HttpGet("{id}")] + public async Task Get(string id) + { + var userId = _userService.GetProperUserId(User).Value; + var organization = await _organizationRepository.GetByIdAsync(new Guid(id), userId); + if(organization == null) + { + throw new NotFoundException(); + } + + return new OrganizationResponseModel(organization); + } + + [HttpGet("")] + public async Task> Get() + { + var userId = _userService.GetProperUserId(User).Value; + var organizations = await _organizationRepository.GetManyByUserIdAsync(userId); + var responses = organizations.Select(o => new OrganizationResponseModel(o)); + return new ListResponseModel(responses); + } + + [HttpPost("")] + public async Task Post([FromBody]OrganizationCreateRequestModel model) + { + var userId = _userService.GetProperUserId(User).Value; + var organization = model.ToOrganization(_userService.GetProperUserId(User).Value); + await _organizationRepository.ReplaceAsync(organization); + return new OrganizationResponseModel(organization); + } + + [HttpPut("{id}")] + [HttpPost("{id}")] + public async Task Put(string id, [FromBody]OrganizationUpdateRequestModel model) + { + var userId = _userService.GetProperUserId(User).Value; + var organization = await _organizationRepository.GetByIdAsync(new Guid(id), userId); + // TODO: Permission checks + if(organization == null) + { + throw new NotFoundException(); + } + + await _organizationRepository.ReplaceAsync(model.ToOrganization(organization)); + return new OrganizationResponseModel(organization); + } + + [HttpDelete("{id}")] + [HttpPost("{id}/delete")] + public async Task Delete(string id) + { + var organization = await _organizationRepository.GetByIdAsync(new Guid(id), + _userService.GetProperUserId(User).Value); + if(organization == null) + { + throw new NotFoundException(); + } + + await _organizationRepository.DeleteAsync(organization); + } + } +} diff --git a/src/Api/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationCreateRequestModel.cs new file mode 100644 index 0000000000..a70a5b635c --- /dev/null +++ b/src/Api/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -0,0 +1,25 @@ +using Bit.Core.Domains; +using Bit.Core.Enums; +using System; + +namespace Bit.Api.Models +{ + public class OrganizationCreateRequestModel + { + public string Name { get; set; } + public PlanType Plan { get; set; } + // TODO: Billing info for paid plans. + + public virtual Organization ToOrganization(Guid userId) + { + var organization = new Organization + { + UserId = userId, + Name = Name, + Plan = Plan + }; + + return organization; + } + } +} diff --git a/src/Api/Models/Request/Organizations/OrganizationUpdateRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationUpdateRequestModel.cs new file mode 100644 index 0000000000..32b9724610 --- /dev/null +++ b/src/Api/Models/Request/Organizations/OrganizationUpdateRequestModel.cs @@ -0,0 +1,15 @@ +using Bit.Core.Domains; + +namespace Bit.Api.Models +{ + public class OrganizationUpdateRequestModel + { + public string Name { get; set; } + + public virtual Organization ToOrganization(Organization existingOrganization) + { + existingOrganization.Name = Name; + return existingOrganization; + } + } +} diff --git a/src/Api/Models/Response/OrganizationResponseModel.cs b/src/Api/Models/Response/OrganizationResponseModel.cs new file mode 100644 index 0000000000..3ab57f0d1a --- /dev/null +++ b/src/Api/Models/Response/OrganizationResponseModel.cs @@ -0,0 +1,28 @@ +using System; +using Bit.Core.Domains; +using Bit.Core.Enums; + +namespace Bit.Api.Models +{ + public class OrganizationResponseModel : ResponseModel + { + public OrganizationResponseModel(Organization organization) + : base("organization") + { + if(organization == null) + { + throw new ArgumentNullException(nameof(organization)); + } + + Id = organization.Id.ToString(); + Name = organization.Name; + Plan = organization.Plan; + MaxUsers = organization.MaxUsers; + } + + public string Id { get; set; } + public string Name { get; set; } + public PlanType Plan { get; set; } + public short MaxUsers { get; set; } + } +} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index bd3d10a2f4..ea69ce2454 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -76,6 +76,8 @@ namespace Bit.Api services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // Context services.AddScoped(); diff --git a/src/Core/Domains/Organization.cs b/src/Core/Domains/Organization.cs new file mode 100644 index 0000000000..1038263f9b --- /dev/null +++ b/src/Core/Domains/Organization.cs @@ -0,0 +1,22 @@ +using System; +using Bit.Core.Utilities; +using Bit.Core.Enums; + +namespace Bit.Core.Domains +{ + public class Organization : IDataObject + { + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string Name { get; set; } + public PlanType Plan { get; set; } + public short MaxUsers { get; set; } + public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; + public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; + + public void SetNewId() + { + Id = CoreHelpers.GenerateComb(); + } + } +} diff --git a/src/Core/Domains/OrganizationUser.cs b/src/Core/Domains/OrganizationUser.cs new file mode 100644 index 0000000000..7bad83549d --- /dev/null +++ b/src/Core/Domains/OrganizationUser.cs @@ -0,0 +1,24 @@ +using System; +using Bit.Core.Utilities; +using Bit.Core.Enums; + +namespace Bit.Core.Domains +{ + public class OrganizationUser : IDataObject + { + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public Guid UserId { get; set; } + public string Email { get; set; } + public string Key { get; set; } + public OrganizationUserStatusType Status { get; set; } + public OrganizationUserType Type { get; set; } + public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; + public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; + + public void SetNewId() + { + Id = CoreHelpers.GenerateComb(); + } + } +} diff --git a/src/Core/Enums/OrganizationUserStatusType.cs b/src/Core/Enums/OrganizationUserStatusType.cs new file mode 100644 index 0000000000..18c8e11af8 --- /dev/null +++ b/src/Core/Enums/OrganizationUserStatusType.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Enums +{ + public enum OrganizationUserStatusType : byte + { + Invited = 0, + Pending = 1, + Accepted = 2, + Confirmed = 3 + } +} diff --git a/src/Core/Enums/OrganizationUserType.cs b/src/Core/Enums/OrganizationUserType.cs new file mode 100644 index 0000000000..91128959ad --- /dev/null +++ b/src/Core/Enums/OrganizationUserType.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Enums +{ + public enum OrganizationUserType : byte + { + Owner = 0, + Admin = 1, + Regular = 2 + } +} diff --git a/src/Core/Enums/PlanType.cs b/src/Core/Enums/PlanType.cs new file mode 100644 index 0000000000..4288aa8282 --- /dev/null +++ b/src/Core/Enums/PlanType.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Enums +{ + public enum PlanType : byte + { + Free = 0, + Family = 1, + Teams = 2, + Enterprise = 3 + } +} diff --git a/src/Core/Repositories/IOrganizationRepository.cs b/src/Core/Repositories/IOrganizationRepository.cs new file mode 100644 index 0000000000..ffe0248549 --- /dev/null +++ b/src/Core/Repositories/IOrganizationRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Domains; + +namespace Bit.Core.Repositories +{ + public interface IOrganizationRepository : IRepository + { + Task GetByIdAsync(Guid id, Guid userId); + Task> GetManyByUserIdAsync(Guid userId); + } +} diff --git a/src/Core/Repositories/IOrganizationUserRepository.cs b/src/Core/Repositories/IOrganizationUserRepository.cs new file mode 100644 index 0000000000..6e31adc93e --- /dev/null +++ b/src/Core/Repositories/IOrganizationUserRepository.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Domains; + +namespace Bit.Core.Repositories +{ + public interface IOrganizationUserRepository : IRepository + { + Task GetByIdAsync(Guid id, Guid userId); + } +} diff --git a/src/Core/Repositories/SqlServer/OrganizationRepository.cs b/src/Core/Repositories/SqlServer/OrganizationRepository.cs new file mode 100644 index 0000000000..bd30fab32d --- /dev/null +++ b/src/Core/Repositories/SqlServer/OrganizationRepository.cs @@ -0,0 +1,48 @@ +using System; +using Bit.Core.Domains; +using System.Threading.Tasks; +using System.Data.SqlClient; +using System.Data; +using Dapper; +using System.Linq; +using System.Collections.Generic; + +namespace Bit.Core.Repositories.SqlServer +{ + public class OrganizationRepository : Repository, IOrganizationRepository + { + public OrganizationRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString) + { } + + public OrganizationRepository(string connectionString) + : base(connectionString) + { } + + public async Task GetByIdAsync(Guid id, Guid userId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[Organization_ReadByIdUserId]", + new { Id = id, UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.SingleOrDefault(); + } + } + + public async Task> GetManyByUserIdAsync(Guid userId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[Organization_ReadUserId]", + new { UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + } +} diff --git a/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs b/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs new file mode 100644 index 0000000000..d3f1cc89c6 --- /dev/null +++ b/src/Core/Repositories/SqlServer/OrganizationUserRepository.cs @@ -0,0 +1,34 @@ +using System; +using Bit.Core.Domains; +using System.Data; +using System.Data.SqlClient; +using System.Threading.Tasks; +using Dapper; +using System.Linq; + +namespace Bit.Core.Repositories.SqlServer +{ + public class OrganizationUserRepository : Repository, IOrganizationUserRepository + { + public OrganizationUserRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString) + { } + + public OrganizationUserRepository(string connectionString) + : base(connectionString) + { } + + public async Task GetByIdAsync(Guid id, Guid userId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationUser_ReadByIdUserId]", + new { Id = id, UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.SingleOrDefault(); + } + } + } +} diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index af5da90b3f..60ea22d62d 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -137,5 +137,10 @@ + + + + + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByIdUserId.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByIdUserId.sql new file mode 100644 index 0000000000..8037bef365 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByIdUserId.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_ReadByIdUserId] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationUserView] + WHERE + [Id] = @Id + AND [UserId] = @UserId +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadByIdUserId.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadByIdUserId.sql new file mode 100644 index 0000000000..1a494b29ba --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Organization_ReadByIdUserId.sql @@ -0,0 +1,17 @@ +CREATE PROCEDURE [dbo].[Organization_ReadByIdUserId] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + O.* + FROM + [dbo].[OrganizationView] O + INNER JOIN + [dbo].[OrganizationUser] OU ON O.[Id] = OU.[OrganizationId] + WHERE + O.[Id] = @Id + AND OU.[UserId] = @UserId +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadByUserId.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadByUserId.sql new file mode 100644 index 0000000000..79325a5cb6 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Organization_ReadByUserId.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[Organization_ReadByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + O.* + FROM + [dbo].[OrganizationView] O + INNER JOIN + [dbo].[OrganizationUser] OU ON O.[Id] = OU.[OrganizationId] + WHERE + OU.[UserId] = @UserId +END \ No newline at end of file diff --git a/src/Sql/dbo/Views/OrganizationUserView.sql b/src/Sql/dbo/Views/OrganizationUserView.sql new file mode 100644 index 0000000000..0cf9fe0938 --- /dev/null +++ b/src/Sql/dbo/Views/OrganizationUserView.sql @@ -0,0 +1,6 @@ +CREATE VIEW [dbo].[OrganizationUserView] +AS +SELECT + * +FROM + [dbo].[OrganizationUser] \ No newline at end of file diff --git a/src/Sql/dbo/Views/OrganizationView.sql b/src/Sql/dbo/Views/OrganizationView.sql new file mode 100644 index 0000000000..bdc1c4c2e7 --- /dev/null +++ b/src/Sql/dbo/Views/OrganizationView.sql @@ -0,0 +1,6 @@ +CREATE VIEW [dbo].[OrganizationView] +AS +SELECT + * +FROM + [dbo].[Organization] \ No newline at end of file