1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

[PM-328] Move files for team-tools (#2857)

* Extract Import-Api endpoints into separate controller

Moved ciphers/import and ciphers/import-organization into new ImportController
Paths have been kept intact for now (no changes on clients needed)
Moved request-models used for import into tools-subfolder

* Update CODEOWNERS for team-tools-dev

* Move HibpController (reports) to tools

* Moving files related to Send

* Moving files related to ReferenceEvent

* Removed unneeded newline
This commit is contained in:
Daniel James Smith
2023-04-18 14:05:17 +02:00
committed by GitHub
parent baec7745f7
commit 4e7b9d2edd
91 changed files with 292 additions and 178 deletions

View File

@ -0,0 +1,97 @@
using System.Net;
using System.Security.Cryptography;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Tools.Controllers;
[Route("hibp")]
[Authorize("Application")]
public class HibpController : Controller
{
private const string HibpBreachApi = "https://haveibeenpwned.com/api/v3/breachedaccount/{0}" +
"?truncateResponse=false&includeUnverified=false";
private static HttpClient _httpClient;
private readonly IUserService _userService;
private readonly ICurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
private readonly string _userAgent;
static HibpController()
{
_httpClient = new HttpClient();
}
public HibpController(
IUserService userService,
ICurrentContext currentContext,
GlobalSettings globalSettings)
{
_userService = userService;
_currentContext = currentContext;
_globalSettings = globalSettings;
_userAgent = _globalSettings.SelfHosted ? "Bitwarden Self-Hosted" : "Bitwarden";
}
[HttpGet("breach")]
public async Task<IActionResult> Get(string username)
{
return await SendAsync(WebUtility.UrlEncode(username), true);
}
private async Task<IActionResult> SendAsync(string username, bool retry)
{
if (!CoreHelpers.SettingHasValue(_globalSettings.HibpApiKey))
{
throw new BadRequestException("HaveIBeenPwned API key not set.");
}
var request = new HttpRequestMessage(HttpMethod.Get, string.Format(HibpBreachApi, username));
request.Headers.Add("hibp-api-key", _globalSettings.HibpApiKey);
request.Headers.Add("hibp-client-id", GetClientId());
request.Headers.Add("User-Agent", _userAgent);
var response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var data = await response.Content.ReadAsStringAsync();
return Content(data, "application/json");
}
else if (response.StatusCode == HttpStatusCode.NotFound)
{
return new NotFoundResult();
}
else if (response.StatusCode == HttpStatusCode.TooManyRequests && retry)
{
var delay = 2000;
if (response.Headers.Contains("retry-after"))
{
var vals = response.Headers.GetValues("retry-after");
if (vals.Any() && int.TryParse(vals.FirstOrDefault(), out var secDelay))
{
delay = (secDelay * 1000) + 200;
}
}
await Task.Delay(delay);
return await SendAsync(username, false);
}
else
{
throw new BadRequestException("Request failed. Status code: " + response.StatusCode);
}
}
private string GetClientId()
{
var userId = _userService.GetProperUserId(User).Value;
using (var sha256 = SHA256.Create())
{
var hash = sha256.ComputeHash(userId.ToByteArray());
return Convert.ToBase64String(hash);
}
}
}

View File

@ -0,0 +1,79 @@
using Bit.Api.Tools.Models.Request.Accounts;
using Bit.Api.Tools.Models.Request.Organizations;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Core.Vault.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Tools.Controllers;
[Route("ciphers")]
[Authorize("Application")]
public class ImportCiphersController : Controller
{
private readonly ICipherService _cipherService;
private readonly IUserService _userService;
private readonly ICurrentContext _currentContext;
private readonly ILogger<ImportCiphersController> _logger;
private readonly GlobalSettings _globalSettings;
public ImportCiphersController(
ICollectionCipherRepository collectionCipherRepository,
ICipherService cipherService,
IUserService userService,
IProviderService providerService,
ICurrentContext currentContext,
ILogger<ImportCiphersController> logger,
GlobalSettings globalSettings)
{
_cipherService = cipherService;
_userService = userService;
_currentContext = currentContext;
_logger = logger;
_globalSettings = globalSettings;
}
[HttpPost("import")]
public async Task PostImport([FromBody] ImportCiphersRequestModel model)
{
if (!_globalSettings.SelfHosted &&
(model.Ciphers.Count() > 6000 || model.FolderRelationships.Count() > 6000 ||
model.Folders.Count() > 1000))
{
throw new BadRequestException("You cannot import this much data at once.");
}
var userId = _userService.GetProperUserId(User).Value;
var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList();
var ciphers = model.Ciphers.Select(c => c.ToCipherDetails(userId, false)).ToList();
await _cipherService.ImportCiphersAsync(folders, ciphers, model.FolderRelationships);
}
[HttpPost("import-organization")]
public async Task PostImport([FromQuery] string organizationId,
[FromBody] ImportOrganizationCiphersRequestModel model)
{
if (!_globalSettings.SelfHosted &&
(model.Ciphers.Count() > 6000 || model.CollectionRelationships.Count() > 12000 ||
model.Collections.Count() > 1000))
{
throw new BadRequestException("You cannot import this much data at once.");
}
var orgId = new Guid(organizationId);
if (!await _currentContext.AccessImportExport(orgId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var collections = model.Collections.Select(c => c.ToCollection(orgId)).ToList();
var ciphers = model.Ciphers.Select(l => l.ToOrganizationCipherDetails(orgId)).ToList();
await _cipherService.ImportCiphersAsync(collections, ciphers, model.CollectionRelationships, userId);
}
}

View File

@ -0,0 +1,340 @@
using System.Text.Json;
using Azure.Messaging.EventGrid;
using Bit.Api.Models.Response;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Tools.Models.Response;
using Bit.Api.Utilities;
using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Tools.Controllers;
[Route("sends")]
[Authorize("Application")]
public class SendsController : Controller
{
private readonly ISendRepository _sendRepository;
private readonly IUserService _userService;
private readonly ISendService _sendService;
private readonly ISendFileStorageService _sendFileStorageService;
private readonly ILogger<SendsController> _logger;
private readonly GlobalSettings _globalSettings;
private readonly ICurrentContext _currentContext;
public SendsController(
ISendRepository sendRepository,
IUserService userService,
ISendService sendService,
ISendFileStorageService sendFileStorageService,
ILogger<SendsController> logger,
GlobalSettings globalSettings,
ICurrentContext currentContext)
{
_sendRepository = sendRepository;
_userService = userService;
_sendService = sendService;
_sendFileStorageService = sendFileStorageService;
_logger = logger;
_globalSettings = globalSettings;
_currentContext = currentContext;
}
[AllowAnonymous]
[HttpPost("access/{id}")]
public async Task<IActionResult> Access(string id, [FromBody] SendAccessRequestModel model)
{
// Uncomment whenever we want to require the `send-id` header
//if (!_currentContext.HttpContext.Request.Headers.ContainsKey("Send-Id") ||
// _currentContext.HttpContext.Request.Headers["Send-Id"] != id)
//{
// throw new BadRequestException("Invalid Send-Id header.");
//}
var guid = new Guid(CoreHelpers.Base64UrlDecode(id));
var (send, passwordRequired, passwordInvalid) =
await _sendService.AccessAsync(guid, model.Password);
if (passwordRequired)
{
return new UnauthorizedResult();
}
if (passwordInvalid)
{
await Task.Delay(2000);
throw new BadRequestException("Invalid password.");
}
if (send == null)
{
throw new NotFoundException();
}
var sendResponse = new SendAccessResponseModel(send, _globalSettings);
if (send.UserId.HasValue && !send.HideEmail.GetValueOrDefault())
{
var creator = await _userService.GetUserByIdAsync(send.UserId.Value);
sendResponse.CreatorIdentifier = creator.Email;
}
return new ObjectResult(sendResponse);
}
[AllowAnonymous]
[HttpPost("{encodedSendId}/access/file/{fileId}")]
public async Task<IActionResult> GetSendFileDownloadData(string encodedSendId,
string fileId, [FromBody] SendAccessRequestModel model)
{
// Uncomment whenever we want to require the `send-id` header
//if (!_currentContext.HttpContext.Request.Headers.ContainsKey("Send-Id") ||
// _currentContext.HttpContext.Request.Headers["Send-Id"] != encodedSendId)
//{
// throw new BadRequestException("Invalid Send-Id header.");
//}
var sendId = new Guid(CoreHelpers.Base64UrlDecode(encodedSendId));
var send = await _sendRepository.GetByIdAsync(sendId);
if (send == null)
{
throw new BadRequestException("Could not locate send");
}
var (url, passwordRequired, passwordInvalid) = await _sendService.GetSendFileDownloadUrlAsync(send, fileId,
model.Password);
if (passwordRequired)
{
return new UnauthorizedResult();
}
if (passwordInvalid)
{
await Task.Delay(2000);
throw new BadRequestException("Invalid password.");
}
if (send == null)
{
throw new NotFoundException();
}
return new ObjectResult(new SendFileDownloadDataResponseModel()
{
Id = fileId,
Url = url,
});
}
[HttpGet("{id}")]
public async Task<SendResponseModel> Get(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var send = await _sendRepository.GetByIdAsync(new Guid(id));
if (send == null || send.UserId != userId)
{
throw new NotFoundException();
}
return new SendResponseModel(send, _globalSettings);
}
[HttpGet("")]
public async Task<ListResponseModel<SendResponseModel>> Get()
{
var userId = _userService.GetProperUserId(User).Value;
var sends = await _sendRepository.GetManyByUserIdAsync(userId);
var responses = sends.Select(s => new SendResponseModel(s, _globalSettings));
return new ListResponseModel<SendResponseModel>(responses);
}
[HttpPost("")]
public async Task<SendResponseModel> Post([FromBody] SendRequestModel model)
{
model.ValidateCreation();
var userId = _userService.GetProperUserId(User).Value;
var send = model.ToSend(userId, _sendService);
await _sendService.SaveSendAsync(send);
return new SendResponseModel(send, _globalSettings);
}
[HttpPost("file")]
[Obsolete("Deprecated File Send API", false)]
[RequestSizeLimit(Constants.FileSize101mb)]
[DisableFormValueModelBinding]
public async Task<SendResponseModel> PostFile()
{
if (!Request?.ContentType.Contains("multipart/") ?? true)
{
throw new BadRequestException("Invalid content.");
}
Send send = null;
await Request.GetSendFileAsync(async (stream, fileName, model) =>
{
model.ValidateCreation();
var userId = _userService.GetProperUserId(User).Value;
var (madeSend, madeData) = model.ToSend(userId, fileName, _sendService);
send = madeSend;
await _sendService.SaveFileSendAsync(send, madeData, model.FileLength.GetValueOrDefault(0));
await _sendService.UploadFileToExistingSendAsync(stream, send);
});
return new SendResponseModel(send, _globalSettings);
}
[HttpPost("file/v2")]
public async Task<SendFileUploadDataResponseModel> PostFile([FromBody] SendRequestModel model)
{
if (model.Type != SendType.File)
{
throw new BadRequestException("Invalid content.");
}
if (!model.FileLength.HasValue)
{
throw new BadRequestException("Invalid content. File size hint is required.");
}
if (model.FileLength.Value > SendService.MAX_FILE_SIZE)
{
throw new BadRequestException($"Max file size is {SendService.MAX_FILE_SIZE_READABLE}.");
}
model.ValidateCreation();
var userId = _userService.GetProperUserId(User).Value;
var (send, data) = model.ToSend(userId, model.File.FileName, _sendService);
var uploadUrl = await _sendService.SaveFileSendAsync(send, data, model.FileLength.Value);
return new SendFileUploadDataResponseModel
{
Url = uploadUrl,
FileUploadType = _sendFileStorageService.FileUploadType,
SendResponse = new SendResponseModel(send, _globalSettings)
};
}
[HttpGet("{id}/file/{fileId}")]
public async Task<SendFileUploadDataResponseModel> RenewFileUpload(string id, string fileId)
{
var userId = _userService.GetProperUserId(User).Value;
var sendId = new Guid(id);
var send = await _sendRepository.GetByIdAsync(sendId);
var fileData = JsonSerializer.Deserialize<SendFileData>(send?.Data);
if (send == null || send.Type != SendType.File || (send.UserId.HasValue && send.UserId.Value != userId) ||
!send.UserId.HasValue || fileData.Id != fileId || fileData.Validated)
{
// Not found if Send isn't found, user doesn't have access, request is faulty,
// or we've already validated the file. This last is to emulate create-only blob permissions for Azure
throw new NotFoundException();
}
return new SendFileUploadDataResponseModel
{
Url = await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId),
FileUploadType = _sendFileStorageService.FileUploadType,
SendResponse = new SendResponseModel(send, _globalSettings),
};
}
[HttpPost("{id}/file/{fileId}")]
[SelfHosted(SelfHostedOnly = true)]
[RequestSizeLimit(Constants.FileSize501mb)]
[DisableFormValueModelBinding]
public async Task PostFileForExistingSend(string id, string fileId)
{
if (!Request?.ContentType.Contains("multipart/") ?? true)
{
throw new BadRequestException("Invalid content.");
}
var send = await _sendRepository.GetByIdAsync(new Guid(id));
await Request.GetFileAsync(async (stream) =>
{
await _sendService.UploadFileToExistingSendAsync(stream, send);
});
}
[AllowAnonymous]
[HttpPost("file/validate/azure")]
public async Task<ObjectResult> AzureValidateFile()
{
return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>
{
{
"Microsoft.Storage.BlobCreated", async (eventGridEvent) =>
{
try
{
var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1];
var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName);
var send = await _sendRepository.GetByIdAsync(new Guid(sendId));
if (send == null)
{
if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService)
{
await azureSendFileStorageService.DeleteBlobAsync(blobName);
}
return;
}
await _sendService.ValidateSendFile(send);
}
catch (Exception e)
{
_logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}");
return;
}
}
}
});
}
[HttpPut("{id}")]
public async Task<SendResponseModel> Put(string id, [FromBody] SendRequestModel model)
{
model.ValidateEdit();
var userId = _userService.GetProperUserId(User).Value;
var send = await _sendRepository.GetByIdAsync(new Guid(id));
if (send == null || send.UserId != userId)
{
throw new NotFoundException();
}
await _sendService.SaveSendAsync(model.ToSend(send, _sendService));
return new SendResponseModel(send, _globalSettings);
}
[HttpPut("{id}/remove-password")]
public async Task<SendResponseModel> PutRemovePassword(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var send = await _sendRepository.GetByIdAsync(new Guid(id));
if (send == null || send.UserId != userId)
{
throw new NotFoundException();
}
send.Password = null;
await _sendService.SaveSendAsync(send);
return new SendResponseModel(send, _globalSettings);
}
[HttpDelete("{id}")]
public async Task Delete(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var send = await _sendRepository.GetByIdAsync(new Guid(id));
if (send == null || send.UserId != userId)
{
throw new NotFoundException();
}
await _sendService.DeleteSendAsync(send);
}
}

View File

@ -0,0 +1,10 @@
using Bit.Api.Vault.Models.Request;
namespace Bit.Api.Tools.Models.Request.Accounts;
public class ImportCiphersRequestModel
{
public FolderWithIdRequestModel[] Folders { get; set; }
public CipherRequestModel[] Ciphers { get; set; }
public KeyValuePair<int, int>[] FolderRelationships { get; set; }
}

View File

@ -0,0 +1,11 @@
using Bit.Api.Models.Request;
using Bit.Api.Vault.Models.Request;
namespace Bit.Api.Tools.Models.Request.Organizations;
public class ImportOrganizationCiphersRequestModel
{
public CollectionWithIdRequestModel[] Collections { get; set; }
public CipherRequestModel[] Ciphers { get; set; }
public KeyValuePair<int, int>[] CollectionRelationships { get; set; }
}

View File

@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Tools.Models.Request;
public class SendAccessRequestModel
{
[StringLength(300)]
public string Password { get; set; }
}

View File

@ -0,0 +1,140 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Core.Exceptions;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
namespace Bit.Api.Tools.Models.Request;
public class SendRequestModel
{
public SendType Type { get; set; }
public long? FileLength { get; set; } = null;
[EncryptedString]
[EncryptedStringLength(1000)]
public string Name { get; set; }
[EncryptedString]
[EncryptedStringLength(1000)]
public string Notes { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public string Key { get; set; }
[Range(1, int.MaxValue)]
public int? MaxAccessCount { get; set; }
public DateTime? ExpirationDate { get; set; }
[Required]
public DateTime? DeletionDate { get; set; }
public SendFileModel File { get; set; }
public SendTextModel Text { get; set; }
[StringLength(1000)]
public string Password { get; set; }
[Required]
public bool? Disabled { get; set; }
public bool? HideEmail { get; set; }
public Send ToSend(Guid userId, ISendService sendService)
{
var send = new Send
{
Type = Type,
UserId = (Guid?)userId
};
ToSend(send, sendService);
return send;
}
public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendService sendService)
{
var send = ToSendBase(new Send
{
Type = Type,
UserId = (Guid?)userId
}, sendService);
var data = new SendFileData(Name, Notes, fileName);
return (send, data);
}
public Send ToSend(Send existingSend, ISendService sendService)
{
existingSend = ToSendBase(existingSend, sendService);
switch (existingSend.Type)
{
case SendType.File:
var fileData = JsonSerializer.Deserialize<SendFileData>(existingSend.Data);
fileData.Name = Name;
fileData.Notes = Notes;
existingSend.Data = JsonSerializer.Serialize(fileData, JsonHelpers.IgnoreWritingNull);
break;
case SendType.Text:
existingSend.Data = JsonSerializer.Serialize(ToSendTextData(), JsonHelpers.IgnoreWritingNull);
break;
default:
throw new ArgumentException("Unsupported type: " + nameof(Type) + ".");
}
return existingSend;
}
public void ValidateCreation()
{
var now = DateTime.UtcNow;
// Add 1 minute for a sane buffer and client clock float
var nowPlus1Minute = now.AddMinutes(1);
if (ExpirationDate.HasValue && ExpirationDate.Value <= nowPlus1Minute)
{
throw new BadRequestException("You cannot create a Send that is already expired. " +
"Adjust the expiration date and try again.");
}
ValidateEdit();
}
public void ValidateEdit()
{
var now = DateTime.UtcNow;
// Add 1 minute for a sane buffer and client clock float
var nowPlus1Minute = now.AddMinutes(1);
if (DeletionDate.HasValue)
{
if (DeletionDate.Value <= nowPlus1Minute)
{
throw new BadRequestException("You cannot have a Send with a deletion date in the past. " +
"Adjust the deletion date and try again.");
}
if (DeletionDate.Value > now.AddDays(31))
{
throw new BadRequestException("You cannot have a Send with a deletion date that far " +
"into the future. Adjust the Deletion Date to a value less than 31 days from now " +
"and try again.");
}
}
}
private Send ToSendBase(Send existingSend, ISendService sendService)
{
existingSend.Key = Key;
existingSend.ExpirationDate = ExpirationDate;
existingSend.DeletionDate = DeletionDate.Value;
existingSend.MaxAccessCount = MaxAccessCount;
if (!string.IsNullOrWhiteSpace(Password))
{
existingSend.Password = sendService.HashPassword(Password);
}
existingSend.Disabled = Disabled.GetValueOrDefault();
existingSend.HideEmail = HideEmail.GetValueOrDefault();
return existingSend;
}
private SendTextData ToSendTextData()
{
return new SendTextData(Name, Notes, Text.Text, Text.Hidden);
}
}
public class SendWithIdRequestModel : SendRequestModel
{
[Required]
public Guid? Id { get; set; }
}

View File

@ -0,0 +1,52 @@
using System.Text.Json;
using Bit.Core.Models.Api;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.Tools.Models.Response;
public class SendAccessResponseModel : ResponseModel
{
public SendAccessResponseModel(Send send, GlobalSettings globalSettings)
: base("send-access")
{
if (send == null)
{
throw new ArgumentNullException(nameof(send));
}
Id = CoreHelpers.Base64UrlEncode(send.Id.ToByteArray());
Type = send.Type;
SendData sendData;
switch (send.Type)
{
case SendType.File:
var fileData = JsonSerializer.Deserialize<SendFileData>(send.Data);
sendData = fileData;
File = new SendFileModel(fileData);
break;
case SendType.Text:
var textData = JsonSerializer.Deserialize<SendTextData>(send.Data);
sendData = textData;
Text = new SendTextModel(textData);
break;
default:
throw new ArgumentException("Unsupported " + nameof(Type) + ".");
}
Name = sendData.Name;
ExpirationDate = send.ExpirationDate;
}
public string Id { get; set; }
public SendType Type { get; set; }
public string Name { get; set; }
public SendFileModel File { get; set; }
public SendTextModel Text { get; set; }
public DateTime? ExpirationDate { get; set; }
public string CreatorIdentifier { get; set; }
}

View File

@ -0,0 +1,11 @@
using Bit.Core.Models.Api;
namespace Bit.Api.Tools.Models.Response;
public class SendFileDownloadDataResponseModel : ResponseModel
{
public string Id { get; set; }
public string Url { get; set; }
public SendFileDownloadDataResponseModel() : base("send-fileDownload") { }
}

View File

@ -0,0 +1,14 @@
using Bit.Core.Enums;
using Bit.Core.Models.Api;
namespace Bit.Api.Tools.Models.Response;
public class SendFileUploadDataResponseModel : ResponseModel
{
public SendFileUploadDataResponseModel() : base("send-fileUpload") { }
public string Url { get; set; }
public FileUploadType FileUploadType { get; set; }
public SendResponseModel SendResponse { get; set; }
}

View File

@ -0,0 +1,71 @@
using System.Text.Json;
using Bit.Core.Models.Api;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.Tools.Models.Response;
public class SendResponseModel : ResponseModel
{
public SendResponseModel(Send send, GlobalSettings globalSettings)
: base("send")
{
if (send == null)
{
throw new ArgumentNullException(nameof(send));
}
Id = send.Id.ToString();
AccessId = CoreHelpers.Base64UrlEncode(send.Id.ToByteArray());
Type = send.Type;
Key = send.Key;
MaxAccessCount = send.MaxAccessCount;
AccessCount = send.AccessCount;
RevisionDate = send.RevisionDate;
ExpirationDate = send.ExpirationDate;
DeletionDate = send.DeletionDate;
Password = send.Password;
Disabled = send.Disabled;
HideEmail = send.HideEmail.GetValueOrDefault();
SendData sendData;
switch (send.Type)
{
case SendType.File:
var fileData = JsonSerializer.Deserialize<SendFileData>(send.Data);
sendData = fileData;
File = new SendFileModel(fileData);
break;
case SendType.Text:
var textData = JsonSerializer.Deserialize<SendTextData>(send.Data);
sendData = textData;
Text = new SendTextModel(textData);
break;
default:
throw new ArgumentException("Unsupported " + nameof(Type) + ".");
}
Name = sendData.Name;
Notes = sendData.Notes;
}
public string Id { get; set; }
public string AccessId { get; set; }
public SendType Type { get; set; }
public string Name { get; set; }
public string Notes { get; set; }
public SendFileModel File { get; set; }
public SendTextModel Text { get; set; }
public string Key { get; set; }
public int? MaxAccessCount { get; set; }
public int AccessCount { get; set; }
public string Password { get; set; }
public bool Disabled { get; set; }
public DateTime RevisionDate { get; set; }
public DateTime? ExpirationDate { get; set; }
public DateTime DeletionDate { get; set; }
public bool HideEmail { get; set; }
}

View File

@ -0,0 +1,26 @@
using System.Text.Json.Serialization;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.Tools.Models;
public class SendFileModel
{
public SendFileModel() { }
public SendFileModel(SendFileData data)
{
Id = data.Id;
FileName = data.FileName;
Size = data.Size;
SizeName = CoreHelpers.ReadableBytesSize(data.Size);
}
public string Id { get; set; }
[EncryptedString]
[EncryptedStringLength(1000)]
public string FileName { get; set; }
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
public long? Size { get; set; }
public string SizeName { get; set; }
}

View File

@ -0,0 +1,20 @@
using Bit.Core.Tools.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.Tools.Models;
public class SendTextModel
{
public SendTextModel() { }
public SendTextModel(SendTextData data)
{
Text = data.Text;
Hidden = data.Hidden;
}
[EncryptedString]
[EncryptedStringLength(1000)]
public string Text { get; set; }
public bool Hidden { get; set; }
}