mirror of
https://github.com/bitwarden/server.git
synced 2024-12-24 17:17:40 +01:00
organization user apis, hardening, completeness
This commit is contained in:
parent
eaeb18a46b
commit
b7254519f0
@ -192,7 +192,7 @@ namespace Bit.Api.Controllers
|
|||||||
if(userId.HasValue)
|
if(userId.HasValue)
|
||||||
{
|
{
|
||||||
var date = await _userService.GetAccountRevisionDateByIdAsync(userId.Value);
|
var date = await _userService.GetAccountRevisionDateByIdAsync(userId.Value);
|
||||||
revisionDate = Core.Utilities.CoreHelpers.EpocMilliseconds(date);
|
revisionDate = Core.Utilities.CoreHelpers.ToEpocMilliseconds(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
return revisionDate;
|
return revisionDate;
|
||||||
|
@ -57,11 +57,19 @@ namespace Bit.Api.Controllers
|
|||||||
[HttpPost("invite")]
|
[HttpPost("invite")]
|
||||||
public async Task Invite(string orgId, [FromBody]OrganizationUserInviteRequestModel model)
|
public async Task Invite(string orgId, [FromBody]OrganizationUserInviteRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var userId = _userService.GetProperUserId(User);
|
||||||
var result = await _organizationService.InviteUserAsync(new Guid(orgId), model.Email, model.Type,
|
var result = await _organizationService.InviteUserAsync(new Guid(orgId), userId.Value, model.Email, model.Type,
|
||||||
model.Subvaults?.Select(s => s.ToSubvaultUser()));
|
model.Subvaults?.Select(s => s.ToSubvaultUser()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}/reinvite")]
|
||||||
|
[HttpPost("{id}/reinvite")]
|
||||||
|
public async Task Reinvite(string orgId, string id)
|
||||||
|
{
|
||||||
|
var userId = _userService.GetProperUserId(User);
|
||||||
|
await _organizationService.ResendInviteAsync(new Guid(orgId), userId.Value, new Guid(id));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPut("{id}/accept")]
|
[HttpPut("{id}/accept")]
|
||||||
[HttpPost("{id}/accept")]
|
[HttpPost("{id}/accept")]
|
||||||
public async Task Accept(string orgId, string id, [FromBody]OrganizationUserAcceptRequestModel model)
|
public async Task Accept(string orgId, string id, [FromBody]OrganizationUserAcceptRequestModel model)
|
||||||
@ -74,12 +82,13 @@ namespace Bit.Api.Controllers
|
|||||||
[HttpPost("{id}/confirm")]
|
[HttpPost("{id}/confirm")]
|
||||||
public async Task Confirm(string orgId, string id, [FromBody]OrganizationUserConfirmRequestModel model)
|
public async Task Confirm(string orgId, string id, [FromBody]OrganizationUserConfirmRequestModel model)
|
||||||
{
|
{
|
||||||
var result = await _organizationService.ConfirmUserAsync(new Guid(id), model.Key);
|
var userId = _userService.GetProperUserId(User);
|
||||||
|
var result = await _organizationService.ConfirmUserAsync(new Guid(orgId), new Guid(id), model.Key, userId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("{id}")]
|
||||||
[HttpPost("{id}")]
|
[HttpPost("{id}")]
|
||||||
public async Task Put(string id, [FromBody]OrganizationUserUpdateRequestModel model)
|
public async Task Put(string orgId, string id, [FromBody]OrganizationUserUpdateRequestModel model)
|
||||||
{
|
{
|
||||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
|
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
|
||||||
if(organizationUser == null)
|
if(organizationUser == null)
|
||||||
@ -87,7 +96,8 @@ namespace Bit.Api.Controllers
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser),
|
var userId = _userService.GetProperUserId(User);
|
||||||
|
await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId.Value,
|
||||||
model.Subvaults?.Select(s => s.ToSubvaultUser()));
|
model.Subvaults?.Select(s => s.ToSubvaultUser()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,14 +105,8 @@ namespace Bit.Api.Controllers
|
|||||||
[HttpPost("{id}/delete")]
|
[HttpPost("{id}/delete")]
|
||||||
public async Task Delete(string orgId, string id)
|
public async Task Delete(string orgId, string id)
|
||||||
{
|
{
|
||||||
var organization = await _organizationRepository.GetByIdAsync(new Guid(id),
|
var userId = _userService.GetProperUserId(User);
|
||||||
_userService.GetProperUserId(User).Value);
|
await _organizationService.DeleteUserAsync(new Guid(orgId), new Guid(id), userId.Value);
|
||||||
if(organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _organizationRepository.DeleteAsync(organization);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,5 +10,6 @@ namespace Bit.Core.Services
|
|||||||
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
||||||
Task SendNoMasterPasswordHintEmailAsync(string email);
|
Task SendNoMasterPasswordHintEmailAsync(string email);
|
||||||
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
||||||
|
Task SendOrganizationInviteEmailAsync(string organizationName, string email, string token);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,10 +9,12 @@ namespace Bit.Core.Services
|
|||||||
public interface IOrganizationService
|
public interface IOrganizationService
|
||||||
{
|
{
|
||||||
Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup organizationSignup);
|
Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup organizationSignup);
|
||||||
Task<OrganizationUser> InviteUserAsync(Guid organizationId, string email, Enums.OrganizationUserType type,
|
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid invitingUserId, string email,
|
||||||
IEnumerable<SubvaultUser> subvaults);
|
Enums.OrganizationUserType type, IEnumerable<SubvaultUser> subvaults);
|
||||||
|
Task ResendInviteAsync(Guid organizationId, Guid invitingUserId, Guid organizationUserId);
|
||||||
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token);
|
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token);
|
||||||
Task<OrganizationUser> ConfirmUserAsync(Guid organizationUserId, string key);
|
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId);
|
||||||
Task SaveUserAsync(OrganizationUser user, IEnumerable<SubvaultUser> subvaults);
|
Task SaveUserAsync(OrganizationUser user, Guid savingUserId, IEnumerable<SubvaultUser> subvaults);
|
||||||
|
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ namespace Bit.Core.Services
|
|||||||
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
|
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
|
||||||
Task GetTwoFactorAsync(User user, Enums.TwoFactorProviderType provider);
|
Task GetTwoFactorAsync(User user, Enums.TwoFactorProviderType provider);
|
||||||
Task<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode);
|
Task<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode);
|
||||||
|
Task<string> GenerateUserTokenAsync(User user, string tokenProvider, string purpose);
|
||||||
Task<IdentityResult> DeleteAsync(User user);
|
Task<IdentityResult> DeleteAsync(User user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Tasks;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Table;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
@ -25,6 +26,11 @@ namespace Bit.Core.Services
|
|||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task SendOrganizationInviteEmailAsync(string organizationName, string email, string token)
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
public Task SendWelcomeEmailAsync(User user)
|
public Task SendWelcomeEmailAsync(User user)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
|
@ -7,6 +7,7 @@ using Bit.Core.Models.Table;
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
@ -17,19 +18,25 @@ namespace Bit.Core.Services
|
|||||||
private readonly ISubvaultRepository _subvaultRepository;
|
private readonly ISubvaultRepository _subvaultRepository;
|
||||||
private readonly ISubvaultUserRepository _subvaultUserRepository;
|
private readonly ISubvaultUserRepository _subvaultUserRepository;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IDataProtector _dataProtector;
|
||||||
|
private readonly IMailService _mailService;
|
||||||
|
|
||||||
public OrganizationService(
|
public OrganizationService(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ISubvaultRepository subvaultRepository,
|
ISubvaultRepository subvaultRepository,
|
||||||
ISubvaultUserRepository subvaultUserRepository,
|
ISubvaultUserRepository subvaultUserRepository,
|
||||||
IUserRepository userRepository)
|
IUserRepository userRepository,
|
||||||
|
IDataProtectionProvider dataProtectionProvider,
|
||||||
|
IMailService mailService)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_subvaultRepository = subvaultRepository;
|
_subvaultRepository = subvaultRepository;
|
||||||
_subvaultUserRepository = subvaultUserRepository;
|
_subvaultUserRepository = subvaultUserRepository;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
|
_dataProtector = dataProtectionProvider.CreateProtector("OrganizationServiceDataProtector");
|
||||||
|
_mailService = mailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup)
|
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup)
|
||||||
@ -90,9 +97,18 @@ namespace Bit.Core.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, string email, Enums.OrganizationUserType type,
|
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid invitingUserId, string email,
|
||||||
IEnumerable<SubvaultUser> subvaults)
|
Enums.OrganizationUserType type, IEnumerable<SubvaultUser> subvaults)
|
||||||
{
|
{
|
||||||
|
if(!(await OrganizationUserHasAdminRightsAsync(organizationId, invitingUserId)))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot invite users.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make sure user is not already invited
|
||||||
|
|
||||||
|
// TODO: validate subvaults?
|
||||||
|
|
||||||
var orgUser = new OrganizationUser
|
var orgUser = new OrganizationUser
|
||||||
{
|
{
|
||||||
OrganizationId = organizationId,
|
OrganizationId = organizationId,
|
||||||
@ -107,21 +123,70 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
await _organizationUserRepository.CreateAsync(orgUser);
|
await _organizationUserRepository.CreateAsync(orgUser);
|
||||||
await SaveUserSubvaultsAsync(orgUser, subvaults, true);
|
await SaveUserSubvaultsAsync(orgUser, subvaults, true);
|
||||||
|
await SendInviteAsync(organizationId, email);
|
||||||
// TODO: send email
|
|
||||||
|
|
||||||
return orgUser;
|
return orgUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ResendInviteAsync(Guid organizationId, Guid invitingUserId, Guid organizationUserId)
|
||||||
|
{
|
||||||
|
if(!(await OrganizationUserHasAdminRightsAsync(organizationId, invitingUserId)))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot invite users.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||||
|
if(orgUser == null || orgUser.OrganizationId != organizationId ||
|
||||||
|
orgUser.Status == Enums.OrganizationUserStatusType.Invited)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("User invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendInviteAsync(organizationId, orgUser.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendInviteAsync(Guid organizationId, string email)
|
||||||
|
{
|
||||||
|
var token = _dataProtector.Protect(
|
||||||
|
$"OrganizationInvite {organizationId} {email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||||
|
|
||||||
|
await _mailService.SendOrganizationInviteEmailAsync("Organization Name", email, token);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token)
|
public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token)
|
||||||
{
|
{
|
||||||
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||||
if(orgUser.Email != user.Email)
|
if(orgUser == null || orgUser.Email != user.Email)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("User invalid.");
|
throw new BadRequestException("User invalid.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: validate token
|
if(orgUser.Status != Enums.OrganizationUserStatusType.Invited)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Already accepted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenValidationFailed = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var unprotectedData = _dataProtector.Unprotect(token);
|
||||||
|
var dataParts = unprotectedData.Split(' ');
|
||||||
|
if(dataParts.Length == 4 && dataParts[0] == "OrganizationInvite" &&
|
||||||
|
new Guid(dataParts[1]) == orgUser.OrganizationId && dataParts[2] == user.Email)
|
||||||
|
{
|
||||||
|
var creationTime = CoreHelpers.FromEpocMilliseconds(Convert.ToInt64(dataParts[3]));
|
||||||
|
tokenValidationFailed = creationTime.AddDays(5) < DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
tokenValidationFailed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(tokenValidationFailed)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid token.");
|
||||||
|
}
|
||||||
|
|
||||||
orgUser.Status = Enums.OrganizationUserStatusType.Accepted;
|
orgUser.Status = Enums.OrganizationUserStatusType.Accepted;
|
||||||
orgUser.UserId = orgUser.Id;
|
orgUser.UserId = orgUser.Id;
|
||||||
@ -133,12 +198,19 @@ namespace Bit.Core.Services
|
|||||||
return orgUser;
|
return orgUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationUserId, string key)
|
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||||
|
Guid confirmingUserId)
|
||||||
{
|
{
|
||||||
|
if(!(await OrganizationUserHasAdminRightsAsync(organizationId, confirmingUserId)))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot confirm users.");
|
||||||
|
}
|
||||||
|
|
||||||
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||||
if(orgUser.Status != Enums.OrganizationUserStatusType.Accepted)
|
if(orgUser == null || orgUser.Status != Enums.OrganizationUserStatusType.Accepted ||
|
||||||
|
orgUser.OrganizationId != organizationId)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("User not accepted.");
|
throw new BadRequestException("User not valid.");
|
||||||
}
|
}
|
||||||
|
|
||||||
orgUser.Status = Enums.OrganizationUserStatusType.Confirmed;
|
orgUser.Status = Enums.OrganizationUserStatusType.Confirmed;
|
||||||
@ -151,17 +223,52 @@ namespace Bit.Core.Services
|
|||||||
return orgUser;
|
return orgUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveUserAsync(OrganizationUser user, IEnumerable<SubvaultUser> subvaults)
|
public async Task SaveUserAsync(OrganizationUser user, Guid savingUserId, IEnumerable<SubvaultUser> subvaults)
|
||||||
{
|
{
|
||||||
if(user.Id.Equals(default(Guid)))
|
if(user.Id.Equals(default(Guid)))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Invite the user first.");
|
throw new BadRequestException("Invite the user first.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!(await OrganizationUserHasAdminRightsAsync(user.OrganizationId, savingUserId)))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot update users.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: validate subvaults?
|
||||||
|
|
||||||
await _organizationUserRepository.ReplaceAsync(user);
|
await _organizationUserRepository.ReplaceAsync(user);
|
||||||
await SaveUserSubvaultsAsync(user, subvaults, false);
|
await SaveUserSubvaultsAsync(user, subvaults, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId)
|
||||||
|
{
|
||||||
|
if(!(await OrganizationUserHasAdminRightsAsync(organizationId, deletingUserId)))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cannot delete users.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||||
|
if(orgUser == null || orgUser.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("User not valid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _organizationUserRepository.DeleteAsync(orgUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> OrganizationUserHasAdminRightsAsync(Guid organizationId, Guid userId)
|
||||||
|
{
|
||||||
|
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
|
||||||
|
if(orgUser == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return orgUser.Status == Enums.OrganizationUserStatusType.Confirmed &&
|
||||||
|
orgUser.Type != Enums.OrganizationUserType.User;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SaveUserSubvaultsAsync(OrganizationUser user, IEnumerable<SubvaultUser> subvaults, bool newUser)
|
private async Task SaveUserSubvaultsAsync(OrganizationUser user, IEnumerable<SubvaultUser> subvaults, bool newUser)
|
||||||
{
|
{
|
||||||
if(subvaults == null)
|
if(subvaults == null)
|
||||||
|
@ -14,6 +14,7 @@ namespace Bit.Core.Services
|
|||||||
private const string ChangeEmailTemplateId = "ec2c1471-8292-4f17-b6b6-8223d514f86e";
|
private const string ChangeEmailTemplateId = "ec2c1471-8292-4f17-b6b6-8223d514f86e";
|
||||||
private const string NoMasterPasswordHintTemplateId = "136eb299-e102-495a-88bd-f96736eea159";
|
private const string NoMasterPasswordHintTemplateId = "136eb299-e102-495a-88bd-f96736eea159";
|
||||||
private const string MasterPasswordHintTemplateId = "be77cfde-95dd-4cb9-b5e0-8286b53885f1";
|
private const string MasterPasswordHintTemplateId = "be77cfde-95dd-4cb9-b5e0-8286b53885f1";
|
||||||
|
private const string OrganizationInviteTemplateId = "1eff5512-e36c-49a8-b9e2-2b215d6bbced";
|
||||||
|
|
||||||
private const string AdministrativeCategoryName = "Administrative";
|
private const string AdministrativeCategoryName = "Administrative";
|
||||||
private const string MarketingCategoryName = "Marketing";
|
private const string MarketingCategoryName = "Marketing";
|
||||||
@ -87,6 +88,19 @@ namespace Bit.Core.Services
|
|||||||
await _client.SendEmailAsync(message);
|
await _client.SendEmailAsync(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SendOrganizationInviteEmailAsync(string organizationName, string email, string token)
|
||||||
|
{
|
||||||
|
var message = CreateDefaultMessage(OrganizationInviteTemplateId);
|
||||||
|
|
||||||
|
message.Subject = $"Join {organizationName}";
|
||||||
|
message.AddTo(new EmailAddress(email));
|
||||||
|
message.AddSubstitution("{{organizationName}}", organizationName);
|
||||||
|
message.AddSubstitution("{{token}}", token);
|
||||||
|
message.AddCategories(new List<string> { AdministrativeCategoryName, "Organization Invite" });
|
||||||
|
|
||||||
|
await _client.SendEmailAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
private SendGridMessage CreateDefaultMessage(string templateId)
|
private SendGridMessage CreateDefaultMessage(string templateId)
|
||||||
{
|
{
|
||||||
var message = new SendGridMessage
|
var message = new SendGridMessage
|
||||||
|
@ -84,9 +84,14 @@ namespace Bit.Core.Utilities
|
|||||||
return cert;
|
return cert;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long EpocMilliseconds(DateTime date)
|
public static long ToEpocMilliseconds(DateTime date)
|
||||||
{
|
{
|
||||||
return (long)Math.Round((date - _epoc).TotalMilliseconds, 0);
|
return (long)Math.Round((date - _epoc).TotalMilliseconds, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DateTime FromEpocMilliseconds(long milliseconds)
|
||||||
|
{
|
||||||
|
return _epoc.AddMilliseconds(milliseconds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user