diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 65c015528c..a0fabf1db9 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -22,6 +22,7 @@ namespace Bit.Api.Controllers private readonly IUserService _userService; private readonly IAttachmentStorageService _attachmentStorageService; private readonly CurrentContext _currentContext; + private readonly GlobalSettings _globalSettings; public CiphersController( ICipherRepository cipherRepository, @@ -29,7 +30,8 @@ namespace Bit.Api.Controllers ICipherService cipherService, IUserService userService, IAttachmentStorageService attachmentStorageService, - CurrentContext currentContext) + CurrentContext currentContext, + GlobalSettings globalSettings) { _cipherRepository = cipherRepository; _collectionCipherRepository = collectionCipherRepository; @@ -37,6 +39,7 @@ namespace Bit.Api.Controllers _userService = userService; _attachmentStorageService = attachmentStorageService; _currentContext = currentContext; + _globalSettings = globalSettings; } [HttpGet("{id}")] @@ -49,7 +52,7 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - return new CipherResponseModel(cipher); + return new CipherResponseModel(cipher, _globalSettings); } [HttpGet("{id}/full-details")] @@ -65,7 +68,7 @@ namespace Bit.Api.Controllers } var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, cipherId); - return new CipherDetailsResponseModel(cipher, collectionCiphers); + return new CipherDetailsResponseModel(cipher, _globalSettings, collectionCiphers); } [HttpGet("")] @@ -73,7 +76,7 @@ namespace Bit.Api.Controllers { var userId = _userService.GetProperUserId(User).Value; var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId); - var responses = ciphers.Select(c => new CipherResponseModel(c)).ToList(); + var responses = ciphers.Select(c => new CipherResponseModel(c, _globalSettings)).ToList(); return new ListResponseModel(responses); } @@ -86,7 +89,7 @@ namespace Bit.Api.Controllers var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(userId); var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); - var responses = ciphers.Select(c => new CipherDetailsResponseModel(c, collectionCiphersGroupDict)); + var responses = ciphers.Select(c => new CipherDetailsResponseModel(c, _globalSettings, collectionCiphersGroupDict)); return new ListResponseModel(responses); } @@ -105,7 +108,8 @@ namespace Bit.Api.Controllers var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(orgIdGuid); var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); - var responses = ciphers.Select(c => new CipherMiniDetailsResponseModel(c, collectionCiphersGroupDict)); + var responses = ciphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings, + collectionCiphersGroupDict)); return new ListResponseModel(responses); } @@ -228,6 +232,11 @@ namespace Bit.Api.Controllers throw new BadRequestException("Invalid content."); } + if(Request.ContentLength > 105906176) // 101 MB, give em' 1 extra MB for cushion + { + throw new BadRequestException("Max file size is 100 MB."); + } + var idGuid = new Guid(id); var userId = _userService.GetProperUserId(User).Value; var cipher = await _cipherRepository.GetByIdAsync(idGuid, userId); diff --git a/src/Api/Controllers/LoginsController.cs b/src/Api/Controllers/LoginsController.cs index 47d3af420b..2c513a34c6 100644 --- a/src/Api/Controllers/LoginsController.cs +++ b/src/Api/Controllers/LoginsController.cs @@ -21,17 +21,20 @@ namespace Bit.Api.Controllers private readonly ICipherService _cipherService; private readonly IUserService _userService; private readonly CurrentContext _currentContext; + private readonly GlobalSettings _globalSettings; public LoginsController( ICipherRepository cipherRepository, ICipherService cipherService, IUserService userService, - CurrentContext currentContext) + CurrentContext currentContext, + GlobalSettings globalSettings) { _cipherRepository = cipherRepository; _cipherService = cipherService; _userService = userService; _currentContext = currentContext; + _globalSettings = globalSettings; } [HttpGet("{id}")] @@ -44,7 +47,7 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - var response = new LoginResponseModel(login); + var response = new LoginResponseModel(login, _globalSettings); return response; } @@ -58,7 +61,7 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - var response = new LoginResponseModel(login); + var response = new LoginResponseModel(login, _globalSettings); return response; } @@ -67,7 +70,7 @@ namespace Bit.Api.Controllers { var userId = _userService.GetProperUserId(User).Value; var logins = await _cipherRepository.GetManyByTypeAndUserIdAsync(Core.Enums.CipherType.Login, userId); - var responses = logins.Select(l => new LoginResponseModel(l)).ToList(); + var responses = logins.Select(l => new LoginResponseModel(l, _globalSettings)).ToList(); return new ListResponseModel(responses); } @@ -78,7 +81,7 @@ namespace Bit.Api.Controllers var login = model.ToCipherDetails(userId); await _cipherService.SaveDetailsAsync(login, userId); - var response = new LoginResponseModel(login); + var response = new LoginResponseModel(login, _globalSettings); return response; } @@ -94,7 +97,7 @@ namespace Bit.Api.Controllers var userId = _userService.GetProperUserId(User).Value; await _cipherService.SaveAsync(login, userId, true); - var response = new LoginResponseModel(login); + var response = new LoginResponseModel(login, _globalSettings); return response; } @@ -118,7 +121,7 @@ namespace Bit.Api.Controllers await _cipherService.SaveDetailsAsync(model.ToCipherDetails(login), userId); - var response = new LoginResponseModel(login); + var response = new LoginResponseModel(login, _globalSettings); return response; } @@ -136,7 +139,7 @@ namespace Bit.Api.Controllers var userId = _userService.GetProperUserId(User).Value; await _cipherService.SaveAsync(model.ToCipher(login), userId, true); - var response = new LoginResponseModel(login); + var response = new LoginResponseModel(login, _globalSettings); return response; } diff --git a/src/Api/settings.json b/src/Api/settings.json index 55e2db4b3b..15004a2ac1 100644 --- a/src/Api/settings.json +++ b/src/Api/settings.json @@ -29,7 +29,7 @@ }, "attachment": { "connectionString": "SECRET", - "baseUrl": "http://localhost:4000/" + "baseUrl": "http://localhost:4000/attachments/" }, "documentDb": { "uri": "SECRET", diff --git a/src/Api/web.config b/src/Api/web.config index 70f7b3ff45..346fdff398 100644 --- a/src/Api/web.config +++ b/src/Api/web.config @@ -7,7 +7,7 @@ - + diff --git a/src/Core/Models/Api/Response/AttachmentResponseModel.cs b/src/Core/Models/Api/Response/AttachmentResponseModel.cs new file mode 100644 index 0000000000..5bc8d38ff4 --- /dev/null +++ b/src/Core/Models/Api/Response/AttachmentResponseModel.cs @@ -0,0 +1,46 @@ +using Bit.Core.Models.Data; +using Bit.Core.Models.Table; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Core.Models.Api +{ + public class AttachmentResponseModel : ResponseModel + { + public AttachmentResponseModel(string id, CipherAttachment.MetaData data, Cipher cipher, GlobalSettings globalSettings) + : base("attachment") + { + Id = id; + Url = $"{globalSettings.Attachment.BaseUrl}{cipher.Id}/{id}"; + FileName = data.FileName; + Size = data.SizeString; + SizeName = Utilities.CoreHelpers.ReadableBytesSize(data.Size); + } + + public string Id { get; set; } + public string Url { get; set; } + public string FileName { get; set; } + public string Size { get; set; } + public string SizeName { get; set; } + + public static IEnumerable FromCipher(Cipher cipher, GlobalSettings globalSettings) + { + if(string.IsNullOrWhiteSpace(cipher.Attachments)) + { + return null; + } + + try + { + var attachments = + JsonConvert.DeserializeObject>(cipher.Attachments); + return attachments.Select(a => new AttachmentResponseModel(a.Key, a.Value, cipher, globalSettings)); + } + catch + { + return null; + } + } + } +} diff --git a/src/Core/Models/Api/Response/CipherResponseModel.cs b/src/Core/Models/Api/Response/CipherResponseModel.cs index 0492214bca..32cd1f7502 100644 --- a/src/Core/Models/Api/Response/CipherResponseModel.cs +++ b/src/Core/Models/Api/Response/CipherResponseModel.cs @@ -8,7 +8,7 @@ namespace Bit.Core.Models.Api { public class CipherMiniResponseModel : ResponseModel { - public CipherMiniResponseModel(Cipher cipher, string obj = "cipherMini") + public CipherMiniResponseModel(Cipher cipher, GlobalSettings globalSettings, string obj = "cipherMini") : base(obj) { if(cipher == null) @@ -20,6 +20,7 @@ namespace Bit.Core.Models.Api Type = cipher.Type; RevisionDate = cipher.RevisionDate; OrganizationId = cipher.OrganizationId?.ToString(); + Attachments = AttachmentResponseModel.FromCipher(cipher, globalSettings); switch(cipher.Type) { @@ -35,13 +36,14 @@ namespace Bit.Core.Models.Api public string OrganizationId { get; set; } public Enums.CipherType Type { get; set; } public dynamic Data { get; set; } + public IEnumerable Attachments { get; set; } public DateTime RevisionDate { get; set; } } public class CipherResponseModel : CipherMiniResponseModel { - public CipherResponseModel(CipherDetails cipher, string obj = "cipher") - : base(cipher, obj) + public CipherResponseModel(CipherDetails cipher, GlobalSettings globalSettings, string obj = "cipher") + : base(cipher, globalSettings, obj) { FolderId = cipher.FolderId?.ToString(); Favorite = cipher.Favorite; @@ -55,9 +57,9 @@ namespace Bit.Core.Models.Api public class CipherDetailsResponseModel : CipherResponseModel { - public CipherDetailsResponseModel(CipherDetails cipher, + public CipherDetailsResponseModel(CipherDetails cipher, GlobalSettings globalSettings, IDictionary> collectionCiphers, string obj = "cipherDetails") - : base(cipher, obj) + : base(cipher, globalSettings, obj) { if(collectionCiphers.ContainsKey(cipher.Id)) { @@ -69,9 +71,9 @@ namespace Bit.Core.Models.Api } } - public CipherDetailsResponseModel(CipherDetails cipher, IEnumerable collectionCiphers, - string obj = "cipherDetails") - : base(cipher, obj) + public CipherDetailsResponseModel(CipherDetails cipher, GlobalSettings globalSettings, + IEnumerable collectionCiphers, string obj = "cipherDetails") + : base(cipher, globalSettings, obj) { CollectionIds = collectionCiphers.Select(c => c.CollectionId); } @@ -81,9 +83,9 @@ namespace Bit.Core.Models.Api public class CipherMiniDetailsResponseModel : CipherMiniResponseModel { - public CipherMiniDetailsResponseModel(Cipher cipher, + public CipherMiniDetailsResponseModel(Cipher cipher, GlobalSettings globalSettings, IDictionary> collectionCiphers, string obj = "cipherMiniDetails") - : base(cipher, obj) + : base(cipher, globalSettings, obj) { if(collectionCiphers.ContainsKey(cipher.Id)) { diff --git a/src/Core/Models/Api/Response/LoginResponseModel.cs b/src/Core/Models/Api/Response/LoginResponseModel.cs index 1fe504513b..c98a21facd 100644 --- a/src/Core/Models/Api/Response/LoginResponseModel.cs +++ b/src/Core/Models/Api/Response/LoginResponseModel.cs @@ -1,12 +1,13 @@ using System; using Core.Models.Data; using Bit.Core.Models.Table; +using System.Collections.Generic; namespace Bit.Core.Models.Api { public class LoginResponseModel : ResponseModel { - public LoginResponseModel(Cipher cipher, string obj = "login") + public LoginResponseModel(Cipher cipher, GlobalSettings globalSettings, string obj = "login") : base(obj) { if(cipher == null) @@ -30,10 +31,11 @@ namespace Bit.Core.Models.Api Notes = data.Notes; RevisionDate = cipher.RevisionDate; Edit = true; + Attachments = AttachmentResponseModel.FromCipher(cipher, globalSettings); } - public LoginResponseModel(CipherDetails cipher, string obj = "login") - : this(cipher as Cipher, obj) + public LoginResponseModel(CipherDetails cipher, GlobalSettings globalSettings, string obj = "login") + : this(cipher as Cipher, globalSettings, obj) { FolderId = cipher.FolderId?.ToString(); Favorite = cipher.Favorite; @@ -50,6 +52,7 @@ namespace Bit.Core.Models.Api public string Username { get; set; } public string Password { get; set; } public string Notes { get; set; } + public IEnumerable Attachments { get; set; } public DateTime RevisionDate { get; set; } } } diff --git a/src/Core/Models/Data/Attachment.cs b/src/Core/Models/Data/CipherAttachment.cs similarity index 100% rename from src/Core/Models/Data/Attachment.cs rename to src/Core/Models/Data/CipherAttachment.cs diff --git a/src/Core/Services/Implementations/AzureAttachmentStorageService.cs b/src/Core/Services/Implementations/AzureAttachmentStorageService.cs index e4cd0fe6aa..5e253b34f3 100644 --- a/src/Core/Services/Implementations/AzureAttachmentStorageService.cs +++ b/src/Core/Services/Implementations/AzureAttachmentStorageService.cs @@ -15,7 +15,7 @@ namespace Bit.Core.Services public AzureAttachmentStorageService( GlobalSettings globalSettings) { - var storageAccount = CloudStorageAccount.Parse(globalSettings.Storage.ConnectionString); + var storageAccount = CloudStorageAccount.Parse(globalSettings.Attachment.ConnectionString); _blobClient = storageAccount.CreateCloudBlobClient(); } @@ -38,7 +38,7 @@ namespace Bit.Core.Services if(_attachmentsContainer == null) { _attachmentsContainer = _blobClient.GetContainerReference(AttchmentContainerName); - await _attachmentsContainer.CreateIfNotExistsAsync(); + await _attachmentsContainer.CreateIfNotExistsAsync(BlobContainerPublicAccessType.Blob, null, null); } } } diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index 69ab0a0d25..ab5f7dab4a 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -204,5 +204,43 @@ namespace Bit.Core.Utilities return sb.ToString(); } } + + // ref: https://stackoverflow.com/a/11124118/1090359 + // Returns the human-readable file size for an arbitrary 64-bit file size . + // The format is "0.## XB", ex: "4.2 KB" or "1.43 GB" + public static string ReadableBytesSize(long size) + { + // Get absolute value + var absoluteSize = (size < 0 ? -size : size); + + // Determine the suffix and readable value + string suffix; + double readable; + if(absoluteSize >= 0x40000000) // 1 Gigabyte + { + suffix = "GB"; + readable = (size >> 20); + } + else if(absoluteSize >= 0x100000) // 1 Megabyte + { + suffix = "MB"; + readable = (size >> 10); + } + else if(absoluteSize >= 0x400) // 1 Kilobyte + { + suffix = "KB"; + readable = size; + } + else + { + return absoluteSize.ToString("0 Bytes"); // Byte + } + + // Divide by 1024 to get fractional value + readable = (readable / 1024); + + // Return formatted number with suffix + return readable.ToString("0.## ") + suffix; + } } }