mirror of
https://github.com/bitwarden/server
synced 2026-01-07 02:53:38 +00:00
Merge branch 'main' into SM-1571-DisableSMAdsForUsers
This commit is contained in:
15
src/Admin/package-lock.json
generated
15
src/Admin/package-lock.json
generated
@@ -18,7 +18,7 @@
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.91.0",
|
||||
"sass": "1.93.2",
|
||||
"sass-loader": "16.0.5",
|
||||
"webpack": "5.101.3",
|
||||
"webpack-cli": "5.1.4"
|
||||
@@ -679,6 +679,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -705,6 +706,7 @@
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -800,6 +802,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001737",
|
||||
"electron-to-chromium": "^1.5.211",
|
||||
@@ -1654,6 +1657,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -1860,11 +1864,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.91.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz",
|
||||
"integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
|
||||
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
@@ -2215,6 +2220,7 @@
|
||||
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
@@ -2264,6 +2270,7 @@
|
||||
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "^0.5.0",
|
||||
"@webpack-cli/configtest": "^2.1.1",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.91.0",
|
||||
"sass": "1.93.2",
|
||||
"sass-loader": "16.0.5",
|
||||
"webpack": "5.101.3",
|
||||
"webpack-cli": "5.1.4"
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Contains organization properties for both OrganizationUsers and ProviderUsers.
|
||||
/// Any organization properties in sync data should be added to this class so they are populated for both
|
||||
/// members and providers.
|
||||
/// </summary>
|
||||
public abstract class BaseProfileOrganizationResponseModel : ResponseModel
|
||||
{
|
||||
protected BaseProfileOrganizationResponseModel(
|
||||
string type, IProfileOrganizationDetails organizationDetails) : base(type)
|
||||
{
|
||||
Id = organizationDetails.OrganizationId;
|
||||
UserId = organizationDetails.UserId;
|
||||
Name = organizationDetails.Name;
|
||||
Enabled = organizationDetails.Enabled;
|
||||
Identifier = organizationDetails.Identifier;
|
||||
ProductTierType = organizationDetails.PlanType.GetProductTier();
|
||||
UsePolicies = organizationDetails.UsePolicies;
|
||||
UseSso = organizationDetails.UseSso;
|
||||
UseKeyConnector = organizationDetails.UseKeyConnector;
|
||||
UseScim = organizationDetails.UseScim;
|
||||
UseGroups = organizationDetails.UseGroups;
|
||||
UseDirectory = organizationDetails.UseDirectory;
|
||||
UseEvents = organizationDetails.UseEvents;
|
||||
UseTotp = organizationDetails.UseTotp;
|
||||
Use2fa = organizationDetails.Use2fa;
|
||||
UseApi = organizationDetails.UseApi;
|
||||
UseResetPassword = organizationDetails.UseResetPassword;
|
||||
UsersGetPremium = organizationDetails.UsersGetPremium;
|
||||
UseCustomPermissions = organizationDetails.UseCustomPermissions;
|
||||
UseActivateAutofillPolicy = organizationDetails.PlanType.GetProductTier() == ProductTierType.Enterprise;
|
||||
UseRiskInsights = organizationDetails.UseRiskInsights;
|
||||
UseOrganizationDomains = organizationDetails.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = organizationDetails.UseAdminSponsoredFamilies;
|
||||
UseAutomaticUserConfirmation = organizationDetails.UseAutomaticUserConfirmation;
|
||||
UseSecretsManager = organizationDetails.UseSecretsManager;
|
||||
UsePasswordManager = organizationDetails.UsePasswordManager;
|
||||
SelfHost = organizationDetails.SelfHost;
|
||||
Seats = organizationDetails.Seats;
|
||||
MaxCollections = organizationDetails.MaxCollections;
|
||||
MaxStorageGb = organizationDetails.MaxStorageGb;
|
||||
Key = organizationDetails.Key;
|
||||
HasPublicAndPrivateKeys = organizationDetails.PublicKey != null && organizationDetails.PrivateKey != null;
|
||||
SsoBound = !string.IsNullOrWhiteSpace(organizationDetails.SsoExternalId);
|
||||
ResetPasswordEnrolled = !string.IsNullOrWhiteSpace(organizationDetails.ResetPasswordKey);
|
||||
ProviderId = organizationDetails.ProviderId;
|
||||
ProviderName = organizationDetails.ProviderName;
|
||||
ProviderType = organizationDetails.ProviderType;
|
||||
LimitCollectionCreation = organizationDetails.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organizationDetails.LimitCollectionDeletion;
|
||||
LimitItemDeletion = organizationDetails.LimitItemDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organizationDetails.AllowAdminAccessToAllCollectionItems;
|
||||
SsoEnabled = organizationDetails.SsoEnabled ?? false;
|
||||
if (organizationDetails.SsoConfig != null)
|
||||
{
|
||||
var ssoConfigData = SsoConfigurationData.Deserialize(organizationDetails.SsoConfig);
|
||||
KeyConnectorEnabled = ssoConfigData.MemberDecryptionType == MemberDecryptionType.KeyConnector && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl);
|
||||
KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;
|
||||
SsoMemberDecryptionType = ssoConfigData.MemberDecryptionType;
|
||||
}
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; } = null!;
|
||||
public bool Enabled { get; set; }
|
||||
public string? Identifier { get; set; }
|
||||
public ProductTierType ProductTierType { get; set; }
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool UseSso { get; set; }
|
||||
public bool UseKeyConnector { get; set; }
|
||||
public bool UseScim { get; set; }
|
||||
public bool UseGroups { get; set; }
|
||||
public bool UseDirectory { get; set; }
|
||||
public bool UseEvents { get; set; }
|
||||
public bool UseTotp { get; set; }
|
||||
public bool Use2fa { get; set; }
|
||||
public bool UseApi { get; set; }
|
||||
public bool UseResetPassword { get; set; }
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public bool UsePasswordManager { get; set; }
|
||||
public bool UsersGetPremium { get; set; }
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
public bool UseActivateAutofillPolicy { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
public bool SelfHost { get; set; }
|
||||
public int? Seats { get; set; }
|
||||
public short? MaxCollections { get; set; }
|
||||
public short? MaxStorageGb { get; set; }
|
||||
public string? Key { get; set; }
|
||||
public bool HasPublicAndPrivateKeys { get; set; }
|
||||
public bool SsoBound { get; set; }
|
||||
public bool ResetPasswordEnrolled { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string? ProviderName { get; set; }
|
||||
public ProviderType? ProviderType { get; set; }
|
||||
public bool SsoEnabled { get; set; }
|
||||
public bool KeyConnectorEnabled { get; set; }
|
||||
public string? KeyConnectorUrl { get; set; }
|
||||
public MemberDecryptionType? SsoMemberDecryptionType { get; set; }
|
||||
public bool AccessSecretsManager { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public OrganizationUserStatusType Status { get; set; }
|
||||
public OrganizationUserType Type { get; set; }
|
||||
public Permissions? Permissions { get; set; }
|
||||
}
|
||||
@@ -1,150 +1,47 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response;
|
||||
|
||||
public class ProfileOrganizationResponseModel : ResponseModel
|
||||
/// <summary>
|
||||
/// Sync data for organization members and their organization.
|
||||
/// Note: see <see cref="ProfileProviderOrganizationResponseModel"/> for organization sync data received by provider users.
|
||||
/// </summary>
|
||||
public class ProfileOrganizationResponseModel : BaseProfileOrganizationResponseModel
|
||||
{
|
||||
public ProfileOrganizationResponseModel(string str) : base(str) { }
|
||||
|
||||
public ProfileOrganizationResponseModel(
|
||||
OrganizationUserOrganizationDetails organization,
|
||||
OrganizationUserOrganizationDetails organizationDetails,
|
||||
IEnumerable<Guid> organizationIdsClaimingUser)
|
||||
: this("profileOrganization")
|
||||
: base("profileOrganization", organizationDetails)
|
||||
{
|
||||
Id = organization.OrganizationId;
|
||||
Name = organization.Name;
|
||||
UsePolicies = organization.UsePolicies;
|
||||
UseSso = organization.UseSso;
|
||||
UseKeyConnector = organization.UseKeyConnector;
|
||||
UseScim = organization.UseScim;
|
||||
UseGroups = organization.UseGroups;
|
||||
UseDirectory = organization.UseDirectory;
|
||||
UseEvents = organization.UseEvents;
|
||||
UseTotp = organization.UseTotp;
|
||||
Use2fa = organization.Use2fa;
|
||||
UseApi = organization.UseApi;
|
||||
UseResetPassword = organization.UseResetPassword;
|
||||
UseSecretsManager = organization.UseSecretsManager;
|
||||
UsePasswordManager = organization.UsePasswordManager;
|
||||
UsersGetPremium = organization.UsersGetPremium;
|
||||
UseCustomPermissions = organization.UseCustomPermissions;
|
||||
UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise;
|
||||
SelfHost = organization.SelfHost;
|
||||
Seats = organization.Seats;
|
||||
MaxCollections = organization.MaxCollections;
|
||||
MaxStorageGb = organization.MaxStorageGb;
|
||||
Key = organization.Key;
|
||||
HasPublicAndPrivateKeys = organization.PublicKey != null && organization.PrivateKey != null;
|
||||
Status = organization.Status;
|
||||
Type = organization.Type;
|
||||
Enabled = organization.Enabled;
|
||||
SsoBound = !string.IsNullOrWhiteSpace(organization.SsoExternalId);
|
||||
Identifier = organization.Identifier;
|
||||
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organization.Permissions);
|
||||
ResetPasswordEnrolled = !string.IsNullOrWhiteSpace(organization.ResetPasswordKey);
|
||||
UserId = organization.UserId;
|
||||
OrganizationUserId = organization.OrganizationUserId;
|
||||
ProviderId = organization.ProviderId;
|
||||
ProviderName = organization.ProviderName;
|
||||
ProviderType = organization.ProviderType;
|
||||
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
|
||||
IsAdminInitiated = organization.IsAdminInitiated ?? false;
|
||||
FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
|
||||
Status = organizationDetails.Status;
|
||||
Type = organizationDetails.Type;
|
||||
OrganizationUserId = organizationDetails.OrganizationUserId;
|
||||
UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organizationDetails.OrganizationId);
|
||||
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationDetails.Permissions);
|
||||
IsAdminInitiated = organizationDetails.IsAdminInitiated ?? false;
|
||||
FamilySponsorshipFriendlyName = organizationDetails.FamilySponsorshipFriendlyName;
|
||||
FamilySponsorshipLastSyncDate = organizationDetails.FamilySponsorshipLastSyncDate;
|
||||
FamilySponsorshipToDelete = organizationDetails.FamilySponsorshipToDelete;
|
||||
FamilySponsorshipValidUntil = organizationDetails.FamilySponsorshipValidUntil;
|
||||
FamilySponsorshipAvailable = (organizationDetails.FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
|
||||
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
||||
.UsersCanSponsor(organization);
|
||||
ProductTierType = organization.PlanType.GetProductTier();
|
||||
FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate;
|
||||
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
|
||||
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;
|
||||
AccessSecretsManager = organization.AccessSecretsManager;
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
LimitItemDeletion = organization.LimitItemDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId);
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
SsoEnabled = organization.SsoEnabled ?? false;
|
||||
|
||||
if (organization.SsoConfig != null)
|
||||
{
|
||||
var ssoConfigData = SsoConfigurationData.Deserialize(organization.SsoConfig);
|
||||
KeyConnectorEnabled = ssoConfigData.MemberDecryptionType == MemberDecryptionType.KeyConnector && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl);
|
||||
KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;
|
||||
SsoMemberDecryptionType = ssoConfigData.MemberDecryptionType;
|
||||
}
|
||||
|
||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
||||
.UsersCanSponsor(organizationDetails);
|
||||
AccessSecretsManager = organizationDetails.AccessSecretsManager;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool UseSso { get; set; }
|
||||
public bool UseKeyConnector { get; set; }
|
||||
public bool UseScim { get; set; }
|
||||
public bool UseGroups { get; set; }
|
||||
public bool UseDirectory { get; set; }
|
||||
public bool UseEvents { get; set; }
|
||||
public bool UseTotp { get; set; }
|
||||
public bool Use2fa { get; set; }
|
||||
public bool UseApi { get; set; }
|
||||
public bool UseResetPassword { get; set; }
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public bool UsePasswordManager { get; set; }
|
||||
public bool UsersGetPremium { get; set; }
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
public bool UseActivateAutofillPolicy { get; set; }
|
||||
public bool SelfHost { get; set; }
|
||||
public int? Seats { get; set; }
|
||||
public short? MaxCollections { get; set; }
|
||||
public short? MaxStorageGb { get; set; }
|
||||
public string Key { get; set; }
|
||||
public OrganizationUserStatusType Status { get; set; }
|
||||
public OrganizationUserType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public bool SsoBound { get; set; }
|
||||
public string Identifier { get; set; }
|
||||
public Permissions Permissions { get; set; }
|
||||
public bool ResetPasswordEnrolled { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public Guid OrganizationUserId { get; set; }
|
||||
public bool HasPublicAndPrivateKeys { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string ProviderName { get; set; }
|
||||
public ProviderType? ProviderType { get; set; }
|
||||
public string FamilySponsorshipFriendlyName { get; set; }
|
||||
public bool UserIsClaimedByOrganization { get; set; }
|
||||
public string? FamilySponsorshipFriendlyName { get; set; }
|
||||
public bool FamilySponsorshipAvailable { get; set; }
|
||||
public ProductTierType ProductTierType { get; set; }
|
||||
public bool KeyConnectorEnabled { get; set; }
|
||||
public string KeyConnectorUrl { get; set; }
|
||||
public DateTime? FamilySponsorshipLastSyncDate { get; set; }
|
||||
public DateTime? FamilySponsorshipValidUntil { get; set; }
|
||||
public bool? FamilySponsorshipToDelete { get; set; }
|
||||
public bool AccessSecretsManager { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool IsAdminInitiated { get; set; }
|
||||
/// <summary>
|
||||
/// Obsolete.
|
||||
/// See <see cref="UserIsClaimedByOrganization"/>
|
||||
/// Obsolete property for backward compatibility
|
||||
/// </summary>
|
||||
[Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")]
|
||||
public bool UserIsManagedByOrganization
|
||||
@@ -152,19 +49,4 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
get => UserIsClaimedByOrganization;
|
||||
set => UserIsClaimedByOrganization = value;
|
||||
}
|
||||
/// <summary>
|
||||
/// Indicates if the user is claimed by the organization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A user is claimed by an organization if the user's email domain is verified by the organization and the user is a member.
|
||||
/// The organization must be enabled and able to have verified domains.
|
||||
/// </remarks>
|
||||
public bool UserIsClaimedByOrganization { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool IsAdminInitiated { get; set; }
|
||||
public bool SsoEnabled { get; set; }
|
||||
public MemberDecryptionType? SsoMemberDecryptionType { get; set; }
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,57 +1,24 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response;
|
||||
|
||||
public class ProfileProviderOrganizationResponseModel : ProfileOrganizationResponseModel
|
||||
/// <summary>
|
||||
/// Sync data for provider users and their managed organizations.
|
||||
/// Note: see <see cref="ProfileOrganizationResponseModel"/> for organization sync data received by organization members.
|
||||
/// </summary>
|
||||
public class ProfileProviderOrganizationResponseModel : BaseProfileOrganizationResponseModel
|
||||
{
|
||||
public ProfileProviderOrganizationResponseModel(ProviderUserOrganizationDetails organization)
|
||||
: base("profileProviderOrganization")
|
||||
public ProfileProviderOrganizationResponseModel(ProviderUserOrganizationDetails organizationDetails)
|
||||
: base("profileProviderOrganization", organizationDetails)
|
||||
{
|
||||
Id = organization.OrganizationId;
|
||||
Name = organization.Name;
|
||||
UsePolicies = organization.UsePolicies;
|
||||
UseSso = organization.UseSso;
|
||||
UseKeyConnector = organization.UseKeyConnector;
|
||||
UseScim = organization.UseScim;
|
||||
UseGroups = organization.UseGroups;
|
||||
UseDirectory = organization.UseDirectory;
|
||||
UseEvents = organization.UseEvents;
|
||||
UseTotp = organization.UseTotp;
|
||||
Use2fa = organization.Use2fa;
|
||||
UseApi = organization.UseApi;
|
||||
UseResetPassword = organization.UseResetPassword;
|
||||
UsersGetPremium = organization.UsersGetPremium;
|
||||
UseCustomPermissions = organization.UseCustomPermissions;
|
||||
UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise;
|
||||
SelfHost = organization.SelfHost;
|
||||
Seats = organization.Seats;
|
||||
MaxCollections = organization.MaxCollections;
|
||||
MaxStorageGb = organization.MaxStorageGb;
|
||||
Key = organization.Key;
|
||||
HasPublicAndPrivateKeys = organization.PublicKey != null && organization.PrivateKey != null;
|
||||
Status = OrganizationUserStatusType.Confirmed; // Provider users are always confirmed
|
||||
Type = OrganizationUserType.Owner; // Provider users behave like Owners
|
||||
Enabled = organization.Enabled;
|
||||
SsoBound = false;
|
||||
Identifier = organization.Identifier;
|
||||
ProviderId = organizationDetails.ProviderId;
|
||||
ProviderName = organizationDetails.ProviderName;
|
||||
ProviderType = organizationDetails.ProviderType;
|
||||
Permissions = new Permissions();
|
||||
ResetPasswordEnrolled = false;
|
||||
UserId = organization.UserId;
|
||||
ProviderId = organization.ProviderId;
|
||||
ProviderName = organization.ProviderName;
|
||||
ProviderType = organization.ProviderType;
|
||||
ProductTierType = organization.PlanType.GetProductTier();
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
LimitItemDeletion = organization.LimitItemDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
||||
AccessSecretsManager = false; // Provider users cannot access Secrets Manager
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Bit.Api.Utilities;
|
||||
|
||||
namespace Bit.Api.Billing.Attributes;
|
||||
|
||||
public class NonTokenizedPaymentMethodTypeValidationAttribute : StringMatchesAttribute
|
||||
{
|
||||
private static readonly string[] _acceptedValues = ["accountCredit"];
|
||||
|
||||
public NonTokenizedPaymentMethodTypeValidationAttribute() : base(_acceptedValues)
|
||||
{
|
||||
ErrorMessage = $"Payment method type must be one of: {string.Join(", ", _acceptedValues)}";
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
namespace Bit.Api.Billing.Attributes;
|
||||
|
||||
public class PaymentMethodTypeValidationAttribute : StringMatchesAttribute
|
||||
public class TokenizedPaymentMethodTypeValidationAttribute : StringMatchesAttribute
|
||||
{
|
||||
private static readonly string[] _acceptedValues = ["bankAccount", "card", "payPal"];
|
||||
|
||||
public PaymentMethodTypeValidationAttribute() : base(_acceptedValues)
|
||||
public TokenizedPaymentMethodTypeValidationAttribute() : base(_acceptedValues)
|
||||
{
|
||||
ErrorMessage = $"Payment method type must be one of: {string.Join(", ", _acceptedValues)}";
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace Bit.Api.Billing.Models.Requests.Payment;
|
||||
public class MinimalTokenizedPaymentMethodRequest
|
||||
{
|
||||
[Required]
|
||||
[PaymentMethodTypeValidation]
|
||||
[TokenizedPaymentMethodTypeValidation]
|
||||
public required string Type { get; set; }
|
||||
|
||||
[Required]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests.Payment;
|
||||
|
||||
public class NonTokenizedPaymentMethodRequest
|
||||
{
|
||||
[Required]
|
||||
[NonTokenizedPaymentMethodTypeValidation]
|
||||
public required string Type { get; set; }
|
||||
|
||||
public NonTokenizedPaymentMethod ToDomain()
|
||||
{
|
||||
return Type switch
|
||||
{
|
||||
"accountCredit" => new NonTokenizedPaymentMethod { Type = NonTokenizablePaymentMethodType.AccountCredit },
|
||||
_ => throw new InvalidOperationException($"Invalid value for {nameof(NonTokenizedPaymentMethod)}.{nameof(NonTokenizedPaymentMethod.Type)}")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ using Bit.Core.Billing.Payment.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests.Premium;
|
||||
|
||||
public class PremiumCloudHostedSubscriptionRequest
|
||||
public class PremiumCloudHostedSubscriptionRequest : IValidatableObject
|
||||
{
|
||||
[Required]
|
||||
public required MinimalTokenizedPaymentMethodRequest TokenizedPaymentMethod { get; set; }
|
||||
public MinimalTokenizedPaymentMethodRequest? TokenizedPaymentMethod { get; set; }
|
||||
public NonTokenizedPaymentMethodRequest? NonTokenizedPaymentMethod { get; set; }
|
||||
|
||||
[Required]
|
||||
public required MinimalBillingAddressRequest BillingAddress { get; set; }
|
||||
@@ -15,11 +15,38 @@ public class PremiumCloudHostedSubscriptionRequest
|
||||
[Range(0, 99)]
|
||||
public short AdditionalStorageGb { get; set; } = 0;
|
||||
|
||||
public (TokenizedPaymentMethod, BillingAddress, short) ToDomain()
|
||||
|
||||
public (PaymentMethod, BillingAddress, short) ToDomain()
|
||||
{
|
||||
var paymentMethod = TokenizedPaymentMethod.ToDomain();
|
||||
// Check if TokenizedPaymentMethod or NonTokenizedPaymentMethod is provided.
|
||||
var tokenizedPaymentMethod = TokenizedPaymentMethod?.ToDomain();
|
||||
var nonTokenizedPaymentMethod = NonTokenizedPaymentMethod?.ToDomain();
|
||||
|
||||
PaymentMethod paymentMethod = tokenizedPaymentMethod != null
|
||||
? tokenizedPaymentMethod
|
||||
: nonTokenizedPaymentMethod!;
|
||||
|
||||
var billingAddress = BillingAddress.ToDomain();
|
||||
|
||||
return (paymentMethod, billingAddress, AdditionalStorageGb);
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (TokenizedPaymentMethod == null && NonTokenizedPaymentMethod == null)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"Either TokenizedPaymentMethod or NonTokenizedPaymentMethod must be provided.",
|
||||
new[] { nameof(TokenizedPaymentMethod), nameof(NonTokenizedPaymentMethod) }
|
||||
);
|
||||
}
|
||||
|
||||
if (TokenizedPaymentMethod != null && NonTokenizedPaymentMethod != null)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"Only one of TokenizedPaymentMethod or NonTokenizedPaymentMethod can be provided.",
|
||||
new[] { nameof(TokenizedPaymentMethod), nameof(NonTokenizedPaymentMethod) }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
|
||||
public class MiscController : Controller
|
||||
{
|
||||
private readonly BitPayClient _bitPayClient;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public MiscController(
|
||||
BitPayClient bitPayClient,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_bitPayClient = bitPayClient;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
[Authorize("Application")]
|
||||
[HttpPost("~/bitpay-invoice")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<string> PostBitPayInvoice([FromBody] BitPayInvoiceRequestModel model)
|
||||
{
|
||||
var invoice = await _bitPayClient.CreateInvoiceAsync(model.ToBitpayInvoice(_globalSettings));
|
||||
return invoice.Url;
|
||||
}
|
||||
|
||||
[Authorize("Application")]
|
||||
[HttpPost("~/setup-payment")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<string> PostSetupPayment()
|
||||
{
|
||||
var options = new SetupIntentCreateOptions
|
||||
{
|
||||
Usage = "off_session"
|
||||
};
|
||||
var service = new SetupIntentService();
|
||||
var setupIntent = await service.CreateAsync(options);
|
||||
return setupIntent.ClientSecret;
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Api.Models.Request;
|
||||
|
||||
public class BitPayInvoiceRequestModel : IValidatableObject
|
||||
{
|
||||
public Guid? UserId { get; set; }
|
||||
public Guid? OrganizationId { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
public bool Credit { get; set; }
|
||||
[Required]
|
||||
public decimal? Amount { get; set; }
|
||||
public string ReturnUrl { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
|
||||
public BitPayLight.Models.Invoice.Invoice ToBitpayInvoice(GlobalSettings globalSettings)
|
||||
{
|
||||
var inv = new BitPayLight.Models.Invoice.Invoice
|
||||
{
|
||||
Price = Convert.ToDouble(Amount.Value),
|
||||
Currency = "USD",
|
||||
RedirectUrl = ReturnUrl,
|
||||
Buyer = new BitPayLight.Models.Invoice.Buyer
|
||||
{
|
||||
Email = Email,
|
||||
Name = Name
|
||||
},
|
||||
NotificationUrl = globalSettings.BitPay.NotificationUrl,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true
|
||||
};
|
||||
|
||||
var posData = string.Empty;
|
||||
if (UserId.HasValue)
|
||||
{
|
||||
posData = "userId:" + UserId.Value;
|
||||
}
|
||||
else if (OrganizationId.HasValue)
|
||||
{
|
||||
posData = "organizationId:" + OrganizationId.Value;
|
||||
}
|
||||
else if (ProviderId.HasValue)
|
||||
{
|
||||
posData = "providerId:" + ProviderId.Value;
|
||||
}
|
||||
|
||||
if (Credit)
|
||||
{
|
||||
posData += ",accountCredit:1";
|
||||
inv.ItemDesc = "Bitwarden Account Credit";
|
||||
}
|
||||
else
|
||||
{
|
||||
inv.ItemDesc = "Bitwarden";
|
||||
}
|
||||
|
||||
inv.PosData = posData;
|
||||
return inv;
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (!UserId.HasValue && !OrganizationId.HasValue && !ProviderId.HasValue)
|
||||
{
|
||||
yield return new ValidationResult("User, Organization or Provider is required.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,9 +94,6 @@ public class Startup
|
||||
services.AddMemoryCache();
|
||||
services.AddDistributedCache(globalSettings);
|
||||
|
||||
// BitPay
|
||||
services.AddSingleton<BitPayClient>();
|
||||
|
||||
if (!globalSettings.SelfHosted)
|
||||
{
|
||||
services.AddIpRateLimiting(globalSettings);
|
||||
|
||||
@@ -64,7 +64,8 @@
|
||||
"bitPay": {
|
||||
"production": false,
|
||||
"token": "SECRET",
|
||||
"notificationUrl": "https://bitwarden.com/SECRET"
|
||||
"notificationUrl": "https://bitwarden.com/SECRET",
|
||||
"webhookKey": "SECRET"
|
||||
},
|
||||
"amazon": {
|
||||
"accessKeyId": "SECRET",
|
||||
|
||||
@@ -8,7 +8,6 @@ public class BillingSettings
|
||||
public virtual string JobsKey { get; set; }
|
||||
public virtual string StripeWebhookKey { get; set; }
|
||||
public virtual string StripeWebhookSecret20250827Basil { get; set; }
|
||||
public virtual string BitPayWebhookKey { get; set; }
|
||||
public virtual string AppleWebhookKey { get; set; }
|
||||
public virtual FreshDeskSettings FreshDesk { get; set; } = new FreshDeskSettings();
|
||||
public virtual string FreshsalesApiKey { get; set; }
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Bit.Billing.Constants;
|
||||
|
||||
public static class BitPayInvoiceStatus
|
||||
{
|
||||
public const string Confirmed = "confirmed";
|
||||
public const string Complete = "complete";
|
||||
}
|
||||
@@ -1,125 +1,79 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Globalization;
|
||||
using Bit.Billing.Constants;
|
||||
using System.Globalization;
|
||||
using Bit.Billing.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Payment.Clients;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using BitPayLight.Models.Invoice;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Billing.Controllers;
|
||||
|
||||
using static BitPayConstants;
|
||||
using static StripeConstants;
|
||||
|
||||
[Route("bitpay")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class BitPayController : Controller
|
||||
public class BitPayController(
|
||||
GlobalSettings globalSettings,
|
||||
IBitPayClient bitPayClient,
|
||||
ITransactionRepository transactionRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IProviderRepository providerRepository,
|
||||
IMailService mailService,
|
||||
IPaymentService paymentService,
|
||||
ILogger<BitPayController> logger,
|
||||
IPremiumUserBillingService premiumUserBillingService)
|
||||
: Controller
|
||||
{
|
||||
private readonly BillingSettings _billingSettings;
|
||||
private readonly BitPayClient _bitPayClient;
|
||||
private readonly ITransactionRepository _transactionRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly ILogger<BitPayController> _logger;
|
||||
private readonly IPremiumUserBillingService _premiumUserBillingService;
|
||||
|
||||
public BitPayController(
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
BitPayClient bitPayClient,
|
||||
ITransactionRepository transactionRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IProviderRepository providerRepository,
|
||||
IMailService mailService,
|
||||
IPaymentService paymentService,
|
||||
ILogger<BitPayController> logger,
|
||||
IPremiumUserBillingService premiumUserBillingService)
|
||||
{
|
||||
_billingSettings = billingSettings?.Value;
|
||||
_bitPayClient = bitPayClient;
|
||||
_transactionRepository = transactionRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_userRepository = userRepository;
|
||||
_providerRepository = providerRepository;
|
||||
_mailService = mailService;
|
||||
_paymentService = paymentService;
|
||||
_logger = logger;
|
||||
_premiumUserBillingService = premiumUserBillingService;
|
||||
}
|
||||
|
||||
[HttpPost("ipn")]
|
||||
public async Task<IActionResult> PostIpn([FromBody] BitPayEventModel model, [FromQuery] string key)
|
||||
{
|
||||
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.BitPayWebhookKey))
|
||||
if (!CoreHelpers.FixedTimeEquals(key, globalSettings.BitPay.WebhookKey))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
if (model == null || string.IsNullOrWhiteSpace(model.Data?.Id) ||
|
||||
string.IsNullOrWhiteSpace(model.Event?.Name))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
return new BadRequestObjectResult("Invalid key");
|
||||
}
|
||||
|
||||
if (model.Event.Name != BitPayNotificationCode.InvoiceConfirmed)
|
||||
{
|
||||
// Only processing confirmed invoice events for now.
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var invoice = await _bitPayClient.GetInvoiceAsync(model.Data.Id);
|
||||
if (invoice == null)
|
||||
{
|
||||
// Request forged...?
|
||||
_logger.LogWarning("Invoice not found. #{InvoiceId}", model.Data.Id);
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
if (invoice.Status != BitPayInvoiceStatus.Confirmed && invoice.Status != BitPayInvoiceStatus.Complete)
|
||||
{
|
||||
_logger.LogWarning("Invoice status of '{InvoiceStatus}' is not acceptable. #{InvoiceId}", invoice.Status, invoice.Id);
|
||||
return new BadRequestResult();
|
||||
}
|
||||
var invoice = await bitPayClient.GetInvoice(model.Data.Id);
|
||||
|
||||
if (invoice.Currency != "USD")
|
||||
{
|
||||
// Only process USD payments
|
||||
_logger.LogWarning("Non USD payment received. #{InvoiceId}", invoice.Id);
|
||||
return new OkResult();
|
||||
logger.LogWarning("Received BitPay invoice webhook for invoice ({InvoiceID}) with non-USD currency: {Currency}", invoice.Id, invoice.Currency);
|
||||
return new BadRequestObjectResult("Cannot process non-USD payments");
|
||||
}
|
||||
|
||||
var (organizationId, userId, providerId) = GetIdsFromPosData(invoice);
|
||||
if (!organizationId.HasValue && !userId.HasValue && !providerId.HasValue)
|
||||
if ((!organizationId.HasValue && !userId.HasValue && !providerId.HasValue) || !invoice.PosData.Contains(PosDataKeys.AccountCredit))
|
||||
{
|
||||
return new OkResult();
|
||||
logger.LogWarning("Received BitPay invoice webhook for invoice ({InvoiceID}) that had invalid POS data: {PosData}", invoice.Id, invoice.PosData);
|
||||
return new BadRequestObjectResult("Invalid POS data");
|
||||
}
|
||||
|
||||
var isAccountCredit = IsAccountCredit(invoice);
|
||||
if (!isAccountCredit)
|
||||
if (invoice.Status != InvoiceStatuses.Complete)
|
||||
{
|
||||
// Only processing credits
|
||||
_logger.LogWarning("Non-credit payment received. #{InvoiceId}", invoice.Id);
|
||||
return new OkResult();
|
||||
logger.LogInformation("Received valid BitPay invoice webhook for invoice ({InvoiceID}) that is not yet complete: {Status}",
|
||||
invoice.Id, invoice.Status);
|
||||
return new OkObjectResult("Waiting for invoice to be completed");
|
||||
}
|
||||
|
||||
var transaction = await _transactionRepository.GetByGatewayIdAsync(GatewayType.BitPay, invoice.Id);
|
||||
if (transaction != null)
|
||||
var existingTransaction = await transactionRepository.GetByGatewayIdAsync(GatewayType.BitPay, invoice.Id);
|
||||
if (existingTransaction != null)
|
||||
{
|
||||
_logger.LogWarning("Already processed this invoice. #{InvoiceId}", invoice.Id);
|
||||
return new OkResult();
|
||||
logger.LogWarning("Already processed BitPay invoice webhook for invoice ({InvoiceID})", invoice.Id);
|
||||
return new OkObjectResult("Invoice already processed");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var tx = new Transaction
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Amount = Convert.ToDecimal(invoice.Price),
|
||||
CreationDate = GetTransactionDate(invoice),
|
||||
@@ -132,50 +86,47 @@ public class BitPayController : Controller
|
||||
PaymentMethodType = PaymentMethodType.BitPay,
|
||||
Details = $"{invoice.Currency}, BitPay {invoice.Id}"
|
||||
};
|
||||
await _transactionRepository.CreateAsync(tx);
|
||||
|
||||
string billingEmail = null;
|
||||
if (tx.OrganizationId.HasValue)
|
||||
await transactionRepository.CreateAsync(transaction);
|
||||
|
||||
var billingEmail = "";
|
||||
if (transaction.OrganizationId.HasValue)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(tx.OrganizationId.Value);
|
||||
if (org != null)
|
||||
var organization = await organizationRepository.GetByIdAsync(transaction.OrganizationId.Value);
|
||||
if (organization != null)
|
||||
{
|
||||
billingEmail = org.BillingEmailAddress();
|
||||
if (await _paymentService.CreditAccountAsync(org, tx.Amount))
|
||||
billingEmail = organization.BillingEmailAddress();
|
||||
if (await paymentService.CreditAccountAsync(organization, transaction.Amount))
|
||||
{
|
||||
await _organizationRepository.ReplaceAsync(org);
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (tx.UserId.HasValue)
|
||||
else if (transaction.UserId.HasValue)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(tx.UserId.Value);
|
||||
var user = await userRepository.GetByIdAsync(transaction.UserId.Value);
|
||||
if (user != null)
|
||||
{
|
||||
billingEmail = user.BillingEmailAddress();
|
||||
await _premiumUserBillingService.Credit(user, tx.Amount);
|
||||
await premiumUserBillingService.Credit(user, transaction.Amount);
|
||||
}
|
||||
}
|
||||
else if (tx.ProviderId.HasValue)
|
||||
else if (transaction.ProviderId.HasValue)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(tx.ProviderId.Value);
|
||||
var provider = await providerRepository.GetByIdAsync(transaction.ProviderId.Value);
|
||||
if (provider != null)
|
||||
{
|
||||
billingEmail = provider.BillingEmailAddress();
|
||||
if (await _paymentService.CreditAccountAsync(provider, tx.Amount))
|
||||
if (await paymentService.CreditAccountAsync(provider, transaction.Amount))
|
||||
{
|
||||
await _providerRepository.ReplaceAsync(provider);
|
||||
await providerRepository.ReplaceAsync(provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Received BitPay account credit transaction that didn't have a user, org, or provider. Invoice#{InvoiceId}", invoice.Id);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(billingEmail))
|
||||
{
|
||||
await _mailService.SendAddedCreditAsync(billingEmail, tx.Amount);
|
||||
await mailService.SendAddedCreditAsync(billingEmail, transaction.Amount);
|
||||
}
|
||||
}
|
||||
// Catch foreign key violations because user/org could have been deleted.
|
||||
@@ -186,58 +137,34 @@ public class BitPayController : Controller
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
private bool IsAccountCredit(BitPayLight.Models.Invoice.Invoice invoice)
|
||||
private static DateTime GetTransactionDate(Invoice invoice)
|
||||
{
|
||||
return invoice != null && invoice.PosData != null && invoice.PosData.Contains("accountCredit:1");
|
||||
var transactions = invoice.Transactions?.Where(transaction =>
|
||||
transaction.Type == null && !string.IsNullOrWhiteSpace(transaction.Confirmations) &&
|
||||
transaction.Confirmations != "0").ToList();
|
||||
|
||||
return transactions?.Count == 1
|
||||
? DateTime.Parse(transactions.First().ReceivedTime, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind)
|
||||
: CoreHelpers.FromEpocMilliseconds(invoice.CurrentTime);
|
||||
}
|
||||
|
||||
private DateTime GetTransactionDate(BitPayLight.Models.Invoice.Invoice invoice)
|
||||
public (Guid? OrganizationId, Guid? UserId, Guid? ProviderId) GetIdsFromPosData(Invoice invoice)
|
||||
{
|
||||
var transactions = invoice.Transactions?.Where(t => t.Type == null &&
|
||||
!string.IsNullOrWhiteSpace(t.Confirmations) && t.Confirmations != "0");
|
||||
if (transactions != null && transactions.Count() == 1)
|
||||
if (invoice.PosData is null or { Length: 0 } || !invoice.PosData.Contains(':'))
|
||||
{
|
||||
return DateTime.Parse(transactions.First().ReceivedTime, CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.RoundtripKind);
|
||||
}
|
||||
return CoreHelpers.FromEpocMilliseconds(invoice.CurrentTime);
|
||||
}
|
||||
|
||||
public Tuple<Guid?, Guid?, Guid?> GetIdsFromPosData(BitPayLight.Models.Invoice.Invoice invoice)
|
||||
{
|
||||
Guid? orgId = null;
|
||||
Guid? userId = null;
|
||||
Guid? providerId = null;
|
||||
|
||||
if (invoice == null || string.IsNullOrWhiteSpace(invoice.PosData) || !invoice.PosData.Contains(':'))
|
||||
{
|
||||
return new Tuple<Guid?, Guid?, Guid?>(null, null, null);
|
||||
return new ValueTuple<Guid?, Guid?, Guid?>(null, null, null);
|
||||
}
|
||||
|
||||
var mainParts = invoice.PosData.Split(',');
|
||||
foreach (var mainPart in mainParts)
|
||||
{
|
||||
var parts = mainPart.Split(':');
|
||||
var ids = invoice.PosData
|
||||
.Split(',')
|
||||
.Select(part => part.Split(':'))
|
||||
.Where(parts => parts.Length == 2 && Guid.TryParse(parts[1], out _))
|
||||
.ToDictionary(parts => parts[0], parts => Guid.Parse(parts[1]));
|
||||
|
||||
if (parts.Length <= 1 || !Guid.TryParse(parts[1], out var id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (parts[0])
|
||||
{
|
||||
case "userId":
|
||||
userId = id;
|
||||
break;
|
||||
case "organizationId":
|
||||
orgId = id;
|
||||
break;
|
||||
case "providerId":
|
||||
providerId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Tuple<Guid?, Guid?, Guid?>(orgId, userId, providerId);
|
||||
return new ValueTuple<Guid?, Guid?, Guid?>(
|
||||
ids.TryGetValue(MetadataKeys.OrganizationId, out var id) ? id : null,
|
||||
ids.TryGetValue(MetadataKeys.UserId, out id) ? id : null,
|
||||
ids.TryGetValue(MetadataKeys.ProviderId, out id) ? id : null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
88
src/Billing/Jobs/ProviderOrganizationDisableJob.cs
Normal file
88
src/Billing/Jobs/ProviderOrganizationDisableJob.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Quartz;
|
||||
|
||||
namespace Bit.Billing.Jobs;
|
||||
|
||||
public class ProviderOrganizationDisableJob(
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IOrganizationDisableCommand organizationDisableCommand,
|
||||
ILogger<ProviderOrganizationDisableJob> logger)
|
||||
: IJob
|
||||
{
|
||||
private const int MaxConcurrency = 5;
|
||||
private const int MaxTimeoutMinutes = 10;
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var providerId = new Guid(context.MergedJobDataMap.GetString("providerId") ?? string.Empty);
|
||||
var expirationDateString = context.MergedJobDataMap.GetString("expirationDate");
|
||||
DateTime? expirationDate = string.IsNullOrEmpty(expirationDateString)
|
||||
? null
|
||||
: DateTime.Parse(expirationDateString);
|
||||
|
||||
logger.LogInformation("Starting to disable organizations for provider {ProviderId}", providerId);
|
||||
|
||||
var startTime = DateTime.UtcNow;
|
||||
var totalProcessed = 0;
|
||||
var totalErrors = 0;
|
||||
|
||||
try
|
||||
{
|
||||
var providerOrganizations = await providerOrganizationRepository
|
||||
.GetManyDetailsByProviderAsync(providerId);
|
||||
|
||||
if (providerOrganizations == null || !providerOrganizations.Any())
|
||||
{
|
||||
logger.LogInformation("No organizations found for provider {ProviderId}", providerId);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Disabling {OrganizationCount} organizations for provider {ProviderId}",
|
||||
providerOrganizations.Count, providerId);
|
||||
|
||||
var semaphore = new SemaphoreSlim(MaxConcurrency, MaxConcurrency);
|
||||
var tasks = providerOrganizations.Select(async po =>
|
||||
{
|
||||
if (DateTime.UtcNow.Subtract(startTime).TotalMinutes > MaxTimeoutMinutes)
|
||||
{
|
||||
logger.LogWarning("Timeout reached while disabling organizations for provider {ProviderId}", providerId);
|
||||
return false;
|
||||
}
|
||||
|
||||
await semaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
await organizationDisableCommand.DisableAsync(po.OrganizationId, expirationDate);
|
||||
Interlocked.Increment(ref totalProcessed);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to disable organization {OrganizationId} for provider {ProviderId}",
|
||||
po.OrganizationId, providerId);
|
||||
Interlocked.Increment(ref totalErrors);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
logger.LogInformation("Completed disabling organizations for provider {ProviderId}. Processed: {TotalProcessed}, Errors: {TotalErrors}",
|
||||
providerId, totalProcessed, totalErrors);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error disabling organizations for provider {ProviderId}. Processed: {TotalProcessed}, Errors: {TotalErrors}",
|
||||
providerId, totalProcessed, totalErrors);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
using Bit.Billing.Constants;
|
||||
using Bit.Billing.Jobs;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Services;
|
||||
using Quartz;
|
||||
using Event = Stripe.Event;
|
||||
namespace Bit.Billing.Services.Implementations;
|
||||
|
||||
@@ -11,17 +15,26 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler
|
||||
private readonly IUserService _userService;
|
||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
||||
private readonly IOrganizationDisableCommand _organizationDisableCommand;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly ISchedulerFactory _schedulerFactory;
|
||||
|
||||
public SubscriptionDeletedHandler(
|
||||
IStripeEventService stripeEventService,
|
||||
IUserService userService,
|
||||
IStripeEventUtilityService stripeEventUtilityService,
|
||||
IOrganizationDisableCommand organizationDisableCommand)
|
||||
IOrganizationDisableCommand organizationDisableCommand,
|
||||
IProviderRepository providerRepository,
|
||||
IProviderService providerService,
|
||||
ISchedulerFactory schedulerFactory)
|
||||
{
|
||||
_stripeEventService = stripeEventService;
|
||||
_userService = userService;
|
||||
_stripeEventUtilityService = stripeEventUtilityService;
|
||||
_organizationDisableCommand = organizationDisableCommand;
|
||||
_providerRepository = providerRepository;
|
||||
_providerService = providerService;
|
||||
_schedulerFactory = schedulerFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -53,9 +66,38 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler
|
||||
|
||||
await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.GetCurrentPeriodEnd());
|
||||
}
|
||||
else if (providerId.HasValue)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId.Value);
|
||||
if (provider != null)
|
||||
{
|
||||
provider.Enabled = false;
|
||||
await _providerService.UpdateAsync(provider);
|
||||
|
||||
await QueueProviderOrganizationDisableJobAsync(providerId.Value, subscription.GetCurrentPeriodEnd());
|
||||
}
|
||||
}
|
||||
else if (userId.HasValue)
|
||||
{
|
||||
await _userService.DisablePremiumAsync(userId.Value, subscription.GetCurrentPeriodEnd());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task QueueProviderOrganizationDisableJobAsync(Guid providerId, DateTime? expirationDate)
|
||||
{
|
||||
var scheduler = await _schedulerFactory.GetScheduler();
|
||||
|
||||
var job = JobBuilder.Create<ProviderOrganizationDisableJob>()
|
||||
.WithIdentity($"disable-provider-orgs-{providerId}", "provider-management")
|
||||
.UsingJobData("providerId", providerId.ToString())
|
||||
.UsingJobData("expirationDate", expirationDate?.ToString("O"))
|
||||
.Build();
|
||||
|
||||
var trigger = TriggerBuilder.Create()
|
||||
.WithIdentity($"disable-trigger-{providerId}", "provider-management")
|
||||
.StartNow()
|
||||
.Build();
|
||||
|
||||
await scheduler.ScheduleJob(job, trigger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,6 @@ public class Startup
|
||||
// Repositories
|
||||
services.AddDatabaseRepositories(globalSettings);
|
||||
|
||||
// BitPay Client
|
||||
services.AddSingleton<BitPayClient>();
|
||||
|
||||
// PayPal IPN Client
|
||||
services.AddHttpClient<IPayPalIPNClient, PayPalIPNClient>();
|
||||
|
||||
|
||||
@@ -23,7 +23,17 @@ public class IntegrationTemplateContext(EventMessage eventMessage)
|
||||
public Guid? CollectionId => Event.CollectionId;
|
||||
public Guid? GroupId => Event.GroupId;
|
||||
public Guid? PolicyId => Event.PolicyId;
|
||||
public Guid? IdempotencyId => Event.IdempotencyId;
|
||||
public Guid? ProviderId => Event.ProviderId;
|
||||
public Guid? ProviderUserId => Event.ProviderUserId;
|
||||
public Guid? ProviderOrganizationId => Event.ProviderOrganizationId;
|
||||
public Guid? InstallationId => Event.InstallationId;
|
||||
public Guid? SecretId => Event.SecretId;
|
||||
public Guid? ProjectId => Event.ProjectId;
|
||||
public Guid? ServiceAccountId => Event.ServiceAccountId;
|
||||
public Guid? GrantedServiceAccountId => Event.GrantedServiceAccountId;
|
||||
|
||||
public string DateIso8601 => Date.ToString("o");
|
||||
public string EventMessage => JsonSerializer.Serialize(Event);
|
||||
|
||||
public User? User { get; set; }
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Interface defining common organization details properties shared between
|
||||
/// regular organization users and provider organization users for profile endpoints.
|
||||
/// </summary>
|
||||
public interface IProfileOrganizationDetails
|
||||
{
|
||||
Guid? UserId { get; set; }
|
||||
Guid OrganizationId { get; set; }
|
||||
string Name { get; set; }
|
||||
bool Enabled { get; set; }
|
||||
PlanType PlanType { get; set; }
|
||||
bool UsePolicies { get; set; }
|
||||
bool UseSso { get; set; }
|
||||
bool UseKeyConnector { get; set; }
|
||||
bool UseScim { get; set; }
|
||||
bool UseGroups { get; set; }
|
||||
bool UseDirectory { get; set; }
|
||||
bool UseEvents { get; set; }
|
||||
bool UseTotp { get; set; }
|
||||
bool Use2fa { get; set; }
|
||||
bool UseApi { get; set; }
|
||||
bool UseResetPassword { get; set; }
|
||||
bool SelfHost { get; set; }
|
||||
bool UsersGetPremium { get; set; }
|
||||
bool UseCustomPermissions { get; set; }
|
||||
bool UseSecretsManager { get; set; }
|
||||
int? Seats { get; set; }
|
||||
short? MaxCollections { get; set; }
|
||||
short? MaxStorageGb { get; set; }
|
||||
string? Identifier { get; set; }
|
||||
string? Key { get; set; }
|
||||
string? ResetPasswordKey { get; set; }
|
||||
string? PublicKey { get; set; }
|
||||
string? PrivateKey { get; set; }
|
||||
string? SsoExternalId { get; set; }
|
||||
string? Permissions { get; set; }
|
||||
Guid? ProviderId { get; set; }
|
||||
string? ProviderName { get; set; }
|
||||
ProviderType? ProviderType { get; set; }
|
||||
bool? SsoEnabled { get; set; }
|
||||
string? SsoConfig { get; set; }
|
||||
bool UsePasswordManager { get; set; }
|
||||
bool LimitCollectionCreation { get; set; }
|
||||
bool LimitCollectionDeletion { get; set; }
|
||||
bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
bool UseRiskInsights { get; set; }
|
||||
bool LimitItemDeletion { get; set; }
|
||||
bool UseAdminSponsoredFamilies { get; set; }
|
||||
bool UseOrganizationDomains { get; set; }
|
||||
bool UseAutomaticUserConfirmation { get; set; }
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserOrganizationDetails
|
||||
public class OrganizationUserOrganizationDetails : IProfileOrganizationDetails
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public Guid OrganizationUserId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool UseSso { get; set; }
|
||||
public bool UseKeyConnector { get; set; }
|
||||
@@ -33,24 +31,24 @@ public class OrganizationUserOrganizationDetails
|
||||
public int? Seats { get; set; }
|
||||
public short? MaxCollections { get; set; }
|
||||
public short? MaxStorageGb { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string? Key { get; set; }
|
||||
public Enums.OrganizationUserStatusType Status { get; set; }
|
||||
public Enums.OrganizationUserType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public PlanType PlanType { get; set; }
|
||||
public string SsoExternalId { get; set; }
|
||||
public string Identifier { get; set; }
|
||||
public string Permissions { get; set; }
|
||||
public string ResetPasswordKey { get; set; }
|
||||
public string PublicKey { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
public string? SsoExternalId { get; set; }
|
||||
public string? Identifier { get; set; }
|
||||
public string? Permissions { get; set; }
|
||||
public string? ResetPasswordKey { get; set; }
|
||||
public string? PublicKey { get; set; }
|
||||
public string? PrivateKey { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string ProviderName { get; set; }
|
||||
public string? ProviderName { get; set; }
|
||||
public ProviderType? ProviderType { get; set; }
|
||||
public string FamilySponsorshipFriendlyName { get; set; }
|
||||
public string? FamilySponsorshipFriendlyName { get; set; }
|
||||
public bool? SsoEnabled { get; set; }
|
||||
public string SsoConfig { get; set; }
|
||||
public string? SsoConfig { get; set; }
|
||||
public DateTime? FamilySponsorshipLastSyncDate { get; set; }
|
||||
public DateTime? FamilySponsorshipValidUntil { get; set; }
|
||||
public bool? FamilySponsorshipToDelete { get; set; }
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
|
||||
public class ProviderUserOrganizationDetails
|
||||
public class ProviderUserOrganizationDetails : IProfileOrganizationDetails
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool UseSso { get; set; }
|
||||
public bool UseKeyConnector { get; set; }
|
||||
@@ -28,20 +25,22 @@ public class ProviderUserOrganizationDetails
|
||||
public bool SelfHost { get; set; }
|
||||
public bool UsersGetPremium { get; set; }
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public bool UsePasswordManager { get; set; }
|
||||
public int? Seats { get; set; }
|
||||
public short? MaxCollections { get; set; }
|
||||
public short? MaxStorageGb { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string? Key { get; set; }
|
||||
public ProviderUserStatusType Status { get; set; }
|
||||
public ProviderUserType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string Identifier { get; set; }
|
||||
public string PublicKey { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
public string? Identifier { get; set; }
|
||||
public string? PublicKey { get; set; }
|
||||
public string? PrivateKey { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
public Guid? ProviderUserId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string ProviderName { get; set; }
|
||||
public string? ProviderName { get; set; }
|
||||
public PlanType PlanType { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
@@ -50,6 +49,11 @@ public class ProviderUserOrganizationDetails
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public ProviderType ProviderType { get; set; }
|
||||
public ProviderType? ProviderType { get; set; }
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
public bool? SsoEnabled { get; set; }
|
||||
public string? SsoConfig { get; set; }
|
||||
public string? SsoExternalId { get; set; }
|
||||
public string? Permissions { get; set; }
|
||||
public string? ResetPasswordKey { get; set; }
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class EmailTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
}
|
||||
|
||||
var code = Encoding.UTF8.GetString(cachedValue);
|
||||
var valid = string.Equals(token, code);
|
||||
var valid = CoreHelpers.FixedTimeEquals(token, code);
|
||||
if (valid)
|
||||
{
|
||||
await _distributedCache.RemoveAsync(cacheKey);
|
||||
|
||||
@@ -64,7 +64,7 @@ public class OtpTokenProvider<TOptions>(
|
||||
}
|
||||
|
||||
var code = Encoding.UTF8.GetString(cachedValue);
|
||||
var valid = string.Equals(token, code);
|
||||
var valid = CoreHelpers.FixedTimeEquals(token, code);
|
||||
if (valid)
|
||||
{
|
||||
await _distributedCache.RemoveAsync(cacheKey);
|
||||
|
||||
14
src/Core/Billing/Constants/BitPayConstants.cs
Normal file
14
src/Core/Billing/Constants/BitPayConstants.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Bit.Core.Billing.Constants;
|
||||
|
||||
public static class BitPayConstants
|
||||
{
|
||||
public static class InvoiceStatuses
|
||||
{
|
||||
public const string Complete = "complete";
|
||||
}
|
||||
|
||||
public static class PosDataKeys
|
||||
{
|
||||
public const string AccountCredit = "accountCredit:1";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Payment.Clients;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Settings;
|
||||
@@ -9,6 +10,8 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Billing.Payment.Commands;
|
||||
|
||||
using static BitPayConstants;
|
||||
|
||||
public interface ICreateBitPayInvoiceForCreditCommand
|
||||
{
|
||||
Task<BillingCommandResult<string>> Run(
|
||||
@@ -31,6 +34,8 @@ public class CreateBitPayInvoiceForCreditCommand(
|
||||
{
|
||||
var (name, email, posData) = GetSubscriberInformation(subscriber);
|
||||
|
||||
var notificationUrl = $"{globalSettings.BitPay.NotificationUrl}?key={globalSettings.BitPay.WebhookKey}";
|
||||
|
||||
var invoice = new Invoice
|
||||
{
|
||||
Buyer = new Buyer { Email = email, Name = name },
|
||||
@@ -38,7 +43,7 @@ public class CreateBitPayInvoiceForCreditCommand(
|
||||
ExtendedNotifications = true,
|
||||
FullNotifications = true,
|
||||
ItemDesc = "Bitwarden",
|
||||
NotificationUrl = globalSettings.BitPay.NotificationUrl,
|
||||
NotificationUrl = notificationUrl,
|
||||
PosData = posData,
|
||||
Price = Convert.ToDouble(amount),
|
||||
RedirectUrl = redirectUrl
|
||||
@@ -51,10 +56,10 @@ public class CreateBitPayInvoiceForCreditCommand(
|
||||
private static (string? Name, string? Email, string POSData) GetSubscriberInformation(
|
||||
ISubscriber subscriber) => subscriber switch
|
||||
{
|
||||
User user => (user.Email, user.Email, $"userId:{user.Id},accountCredit:1"),
|
||||
User user => (user.Email, user.Email, $"userId:{user.Id},{PosDataKeys.AccountCredit}"),
|
||||
Organization organization => (organization.Name, organization.BillingEmail,
|
||||
$"organizationId:{organization.Id},accountCredit:1"),
|
||||
Provider provider => (provider.Name, provider.BillingEmail, $"providerId:{provider.Id},accountCredit:1"),
|
||||
$"organizationId:{organization.Id},{PosDataKeys.AccountCredit}"),
|
||||
Provider provider => (provider.Name, provider.BillingEmail, $"providerId:{provider.Id},{PosDataKeys.AccountCredit}"),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(subscriber))
|
||||
};
|
||||
}
|
||||
|
||||
11
src/Core/Billing/Payment/Models/NonTokenizedPaymentMethod.cs
Normal file
11
src/Core/Billing/Payment/Models/NonTokenizedPaymentMethod.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.Billing.Payment.Models;
|
||||
|
||||
public record NonTokenizedPaymentMethod
|
||||
{
|
||||
public NonTokenizablePaymentMethodType Type { get; set; }
|
||||
}
|
||||
|
||||
public enum NonTokenizablePaymentMethodType
|
||||
{
|
||||
AccountCredit,
|
||||
}
|
||||
69
src/Core/Billing/Payment/Models/PaymentMethod.cs
Normal file
69
src/Core/Billing/Payment/Models/PaymentMethod.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using OneOf;
|
||||
|
||||
namespace Bit.Core.Billing.Payment.Models;
|
||||
|
||||
[JsonConverter(typeof(PaymentMethodJsonConverter))]
|
||||
public class PaymentMethod(OneOf<TokenizedPaymentMethod, NonTokenizedPaymentMethod> input)
|
||||
: OneOfBase<TokenizedPaymentMethod, NonTokenizedPaymentMethod>(input)
|
||||
{
|
||||
public static implicit operator PaymentMethod(TokenizedPaymentMethod tokenized) => new(tokenized);
|
||||
public static implicit operator PaymentMethod(NonTokenizedPaymentMethod nonTokenized) => new(nonTokenized);
|
||||
public bool IsTokenized => IsT0;
|
||||
public bool IsNonTokenized => IsT1;
|
||||
}
|
||||
|
||||
internal class PaymentMethodJsonConverter : JsonConverter<PaymentMethod>
|
||||
{
|
||||
public override PaymentMethod Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var element = JsonElement.ParseValue(ref reader);
|
||||
|
||||
if (!element.TryGetProperty("type", out var typeProperty))
|
||||
{
|
||||
throw new JsonException("PaymentMethod requires a 'type' property");
|
||||
}
|
||||
|
||||
var type = typeProperty.GetString();
|
||||
|
||||
|
||||
if (Enum.TryParse<TokenizablePaymentMethodType>(type, true, out var tokenizedType) &&
|
||||
Enum.IsDefined(typeof(TokenizablePaymentMethodType), tokenizedType))
|
||||
{
|
||||
var token = element.TryGetProperty("token", out var tokenProperty) ? tokenProperty.GetString() : null;
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
throw new JsonException("TokenizedPaymentMethod requires a 'token' property");
|
||||
}
|
||||
|
||||
return new TokenizedPaymentMethod { Type = tokenizedType, Token = token };
|
||||
}
|
||||
|
||||
if (Enum.TryParse<NonTokenizablePaymentMethodType>(type, true, out var nonTokenizedType) &&
|
||||
Enum.IsDefined(typeof(NonTokenizablePaymentMethodType), nonTokenizedType))
|
||||
{
|
||||
return new NonTokenizedPaymentMethod { Type = nonTokenizedType };
|
||||
}
|
||||
|
||||
throw new JsonException($"Unknown payment method type: {type}");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, PaymentMethod value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
|
||||
value.Switch(
|
||||
tokenized =>
|
||||
{
|
||||
writer.WriteString("type",
|
||||
tokenized.Type.ToString().ToLowerInvariant()
|
||||
);
|
||||
writer.WriteString("token", tokenized.Token);
|
||||
},
|
||||
nonTokenized => { writer.WriteString("type", nonTokenized.Type.ToString().ToLowerInvariant()); }
|
||||
);
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using Microsoft.Extensions.Logging;
|
||||
using OneOf.Types;
|
||||
using Stripe;
|
||||
using Customer = Stripe.Customer;
|
||||
using PaymentMethod = Bit.Core.Billing.Payment.Models.PaymentMethod;
|
||||
using Subscription = Stripe.Subscription;
|
||||
|
||||
namespace Bit.Core.Billing.Premium.Commands;
|
||||
@@ -38,7 +39,7 @@ public interface ICreatePremiumCloudHostedSubscriptionCommand
|
||||
/// <returns>A billing command result indicating success or failure with appropriate error details.</returns>
|
||||
Task<BillingCommandResult<None>> Run(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
PaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress,
|
||||
short additionalStorageGb);
|
||||
}
|
||||
@@ -60,7 +61,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
|
||||
public Task<BillingCommandResult<None>> Run(
|
||||
User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
PaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress,
|
||||
short additionalStorageGb) => HandleAsync<None>(async () =>
|
||||
{
|
||||
@@ -74,6 +75,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
return new BadRequest("Additional storage must be greater than 0.");
|
||||
}
|
||||
|
||||
// Note: A customer will already exist if the customer has purchased account credits.
|
||||
var customer = string.IsNullOrEmpty(user.GatewayCustomerId)
|
||||
? await CreateCustomerAsync(user, paymentMethod, billingAddress)
|
||||
: await subscriberService.GetCustomerOrThrow(user, new CustomerGetOptions { Expand = _expand });
|
||||
@@ -82,18 +84,31 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
|
||||
var subscription = await CreateSubscriptionAsync(user.Id, customer, additionalStorageGb > 0 ? additionalStorageGb : null);
|
||||
|
||||
switch (paymentMethod)
|
||||
{
|
||||
case { Type: TokenizablePaymentMethodType.PayPal }
|
||||
when subscription.Status == StripeConstants.SubscriptionStatus.Incomplete:
|
||||
case { Type: not TokenizablePaymentMethodType.PayPal }
|
||||
when subscription.Status == StripeConstants.SubscriptionStatus.Active:
|
||||
paymentMethod.Switch(
|
||||
tokenized =>
|
||||
{
|
||||
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
|
||||
switch (tokenized)
|
||||
{
|
||||
case { Type: TokenizablePaymentMethodType.PayPal }
|
||||
when subscription.Status == StripeConstants.SubscriptionStatus.Incomplete:
|
||||
case { Type: not TokenizablePaymentMethodType.PayPal }
|
||||
when subscription.Status == StripeConstants.SubscriptionStatus.Active:
|
||||
{
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
nonTokenized =>
|
||||
{
|
||||
if (subscription.Status == StripeConstants.SubscriptionStatus.Active)
|
||||
{
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
user.GatewayCustomerId = customer.Id;
|
||||
@@ -109,9 +124,15 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
});
|
||||
|
||||
private async Task<Customer> CreateCustomerAsync(User user,
|
||||
TokenizedPaymentMethod paymentMethod,
|
||||
PaymentMethod paymentMethod,
|
||||
BillingAddress billingAddress)
|
||||
{
|
||||
if (paymentMethod.IsNonTokenized)
|
||||
{
|
||||
_logger.LogError("Cannot create customer for user ({UserID}) using non-tokenized payment method. The customer should already exist", user.Id);
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var subscriberName = user.SubscriberName();
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
@@ -153,13 +174,14 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
|
||||
var braintreeCustomerId = "";
|
||||
|
||||
// We have checked that the payment method is tokenized, so we can safely cast it.
|
||||
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
|
||||
switch (paymentMethod.Type)
|
||||
switch (paymentMethod.AsT0.Type)
|
||||
{
|
||||
case TokenizablePaymentMethodType.BankAccount:
|
||||
{
|
||||
var setupIntent =
|
||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = paymentMethod.Token }))
|
||||
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = paymentMethod.AsT0.Token }))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (setupIntent == null)
|
||||
@@ -173,19 +195,19 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
}
|
||||
case TokenizablePaymentMethodType.Card:
|
||||
{
|
||||
customerCreateOptions.PaymentMethod = paymentMethod.Token;
|
||||
customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = paymentMethod.Token;
|
||||
customerCreateOptions.PaymentMethod = paymentMethod.AsT0.Token;
|
||||
customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = paymentMethod.AsT0.Token;
|
||||
break;
|
||||
}
|
||||
case TokenizablePaymentMethodType.PayPal:
|
||||
{
|
||||
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(user, paymentMethod.Token);
|
||||
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(user, paymentMethod.AsT0.Token);
|
||||
customerCreateOptions.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
_logger.LogError("Cannot create customer for user ({UserID}) using payment method type ({PaymentMethodType}) as it is not supported", user.Id, paymentMethod.Type.ToString());
|
||||
_logger.LogError("Cannot create customer for user ({UserID}) using payment method type ({PaymentMethodType}) as it is not supported", user.Id, paymentMethod.AsT0.Type.ToString());
|
||||
throw new BillingException();
|
||||
}
|
||||
}
|
||||
@@ -203,18 +225,21 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
async Task Revert()
|
||||
{
|
||||
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||
switch (paymentMethod.Type)
|
||||
if (paymentMethod.IsTokenized)
|
||||
{
|
||||
case TokenizablePaymentMethodType.BankAccount:
|
||||
{
|
||||
await setupIntentCache.RemoveSetupIntentForSubscriber(user.Id);
|
||||
break;
|
||||
}
|
||||
case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
||||
{
|
||||
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
||||
break;
|
||||
}
|
||||
switch (paymentMethod.AsT0.Type)
|
||||
{
|
||||
case TokenizablePaymentMethodType.BankAccount:
|
||||
{
|
||||
await setupIntentCache.RemoveSetupIntentForSubscriber(user.Id);
|
||||
break;
|
||||
}
|
||||
case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
||||
{
|
||||
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ public static class FeatureFlagKeys
|
||||
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
||||
public const string Otp6Digits = "pm-18612-otp-6-digits";
|
||||
public const string FailedTwoFactorEmail = "pm-24425-send-2fa-failed-email";
|
||||
public const string PM24579_PreventSsoOnExistingNonCompliantUsers = "pm-24579-prevent-sso-on-existing-non-compliant-users";
|
||||
public const string DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods";
|
||||
public const string PM23174ManageAccountRecoveryPermissionDrivesTheNeedToSetMasterPassword =
|
||||
"pm-23174-manage-account-recovery-permission-drives-the-need-to-set-master-password";
|
||||
@@ -241,6 +242,7 @@ public static class FeatureFlagKeys
|
||||
public const string PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view";
|
||||
public const string PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp";
|
||||
public const string PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption";
|
||||
public const string PM23904_RiskInsightsForPremium = "pm-23904-risk-insights-for-premium";
|
||||
|
||||
/* Innovation Team */
|
||||
public const string ArchiveVaultItems = "pm-19148-innovation-archive";
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="licensing.cer" />
|
||||
<EmbeddedResource Include="licensing_dev.cer" />
|
||||
<EmbeddedResource Include="MailTemplates\Handlebars\**\*.hbs" />
|
||||
|
||||
<!-- Email templates uses .hbs extension, they must be included for emails to work -->
|
||||
<EmbeddedResource Include="**\*.hbs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -72,7 +74,7 @@
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Resources\" />
|
||||
<Folder Include="Properties\" />
|
||||
|
||||
@@ -11,12 +11,24 @@ public class OrganizationReport : ITableObject<Guid>
|
||||
public Guid OrganizationId { get; set; }
|
||||
public string ReportData { get; set; } = string.Empty;
|
||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public string ContentEncryptionKey { get; set; } = string.Empty;
|
||||
|
||||
public string? SummaryData { get; set; } = null;
|
||||
public string? ApplicationData { get; set; } = null;
|
||||
public string? SummaryData { get; set; }
|
||||
public string? ApplicationData { get; set; }
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
public int? ApplicationCount { get; set; }
|
||||
public int? ApplicationAtRiskCount { get; set; }
|
||||
public int? CriticalApplicationCount { get; set; }
|
||||
public int? CriticalApplicationAtRiskCount { get; set; }
|
||||
public int? MemberCount { get; set; }
|
||||
public int? MemberAtRiskCount { get; set; }
|
||||
public int? CriticalMemberCount { get; set; }
|
||||
public int? CriticalMemberAtRiskCount { get; set; }
|
||||
public int? PasswordCount { get; set; }
|
||||
public int? PasswordAtRiskCount { get; set; }
|
||||
public int? CriticalPasswordCount { get; set; }
|
||||
public int? CriticalPasswordAtRiskCount { get; set; }
|
||||
|
||||
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
|
||||
2373
src/Core/MailTemplates/Mjml/package-lock.json
generated
2373
src/Core/MailTemplates/Mjml/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@
|
||||
"prettier": "prettier --cache --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"mjml": "4.15.3",
|
||||
"mjml": "4.16.1",
|
||||
"mjml-core": "4.15.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
54
src/Core/Platform/Mailer/BaseMail.cs
Normal file
54
src/Core/Platform/Mailer/BaseMail.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace Bit.Core.Platform.Mailer;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// BaseMail describes a model for emails. It contains metadata about the email such as recipients,
|
||||
/// subject, and an optional category for processing at the upstream email delivery service.
|
||||
///
|
||||
/// Each BaseMail must have a view model that inherits from BaseMailView. The view model is used to
|
||||
/// generate the text part and HTML body.
|
||||
/// </summary>
|
||||
public abstract class BaseMail<TView> where TView : BaseMailView
|
||||
{
|
||||
/// <summary>
|
||||
/// Email recipients.
|
||||
/// </summary>
|
||||
public required IEnumerable<string> ToEmails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The subject of the email.
|
||||
/// </summary>
|
||||
public abstract string Subject { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An optional category for processing at the upstream email delivery service.
|
||||
/// </summary>
|
||||
public string? Category { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Allows you to override and ignore the suppression list for this email.
|
||||
///
|
||||
/// Warning: This should be used with caution, valid reasons are primarily account recovery, email OTP.
|
||||
/// </summary>
|
||||
public virtual bool IgnoreSuppressList { get; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// View model for the email body.
|
||||
/// </summary>
|
||||
public required TView View { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Each MailView consists of two body parts: a text part and an HTML part and the filename must be
|
||||
/// relative to the viewmodel and match the following pattern:
|
||||
/// - `{ClassName}.html.hbs` for the HTML part
|
||||
/// - `{ClassName}.text.hbs` for the text part
|
||||
/// </summary>
|
||||
public abstract class BaseMailView
|
||||
{
|
||||
/// <summary>
|
||||
/// Current year.
|
||||
/// </summary>
|
||||
public string CurrentYear => DateTime.UtcNow.Year.ToString();
|
||||
}
|
||||
80
src/Core/Platform/Mailer/HandlebarMailRenderer.cs
Normal file
80
src/Core/Platform/Mailer/HandlebarMailRenderer.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using HandlebarsDotNet;
|
||||
|
||||
namespace Bit.Core.Platform.Mailer;
|
||||
|
||||
public class HandlebarMailRenderer : IMailRenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// Lazy-initialized Handlebars instance. Thread-safe and ensures initialization occurs only once.
|
||||
/// </summary>
|
||||
private readonly Lazy<Task<IHandlebars>> _handlebarsTask = new(InitializeHandlebarsAsync, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
|
||||
/// <summary>
|
||||
/// Helper function that returns the handlebar instance.
|
||||
/// </summary>
|
||||
private Task<IHandlebars> GetHandlebars() => _handlebarsTask.Value;
|
||||
|
||||
/// <summary>
|
||||
/// This dictionary is used to cache compiled templates in a thread-safe manner.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, Lazy<Task<HandlebarsTemplate<object, object>>>> _templateCache = new();
|
||||
|
||||
public async Task<(string html, string txt)> RenderAsync(BaseMailView model)
|
||||
{
|
||||
var html = await CompileTemplateAsync(model, "html");
|
||||
var txt = await CompileTemplateAsync(model, "text");
|
||||
|
||||
return (html, txt);
|
||||
}
|
||||
|
||||
private async Task<string> CompileTemplateAsync(BaseMailView model, string type)
|
||||
{
|
||||
var templateName = $"{model.GetType().FullName}.{type}.hbs";
|
||||
var assembly = model.GetType().Assembly;
|
||||
|
||||
// GetOrAdd is atomic - only one Lazy will be stored per templateName.
|
||||
// The Lazy with ExecutionAndPublication ensures the compilation happens exactly once.
|
||||
var lazyTemplate = _templateCache.GetOrAdd(
|
||||
templateName,
|
||||
key => new Lazy<Task<HandlebarsTemplate<object, object>>>(
|
||||
() => CompileTemplateInternalAsync(assembly, key),
|
||||
LazyThreadSafetyMode.ExecutionAndPublication));
|
||||
|
||||
var template = await lazyTemplate.Value;
|
||||
return template(model);
|
||||
}
|
||||
|
||||
private async Task<HandlebarsTemplate<object, object>> CompileTemplateInternalAsync(Assembly assembly, string templateName)
|
||||
{
|
||||
var source = await ReadSourceAsync(assembly, templateName);
|
||||
var handlebars = await GetHandlebars();
|
||||
return handlebars.Compile(source);
|
||||
}
|
||||
|
||||
private static async Task<string> ReadSourceAsync(Assembly assembly, string template)
|
||||
{
|
||||
if (assembly.GetManifestResourceNames().All(f => f != template))
|
||||
{
|
||||
throw new FileNotFoundException("Template not found: " + template);
|
||||
}
|
||||
|
||||
await using var s = assembly.GetManifestResourceStream(template)!;
|
||||
using var sr = new StreamReader(s);
|
||||
return await sr.ReadToEndAsync();
|
||||
}
|
||||
|
||||
private static async Task<IHandlebars> InitializeHandlebarsAsync()
|
||||
{
|
||||
var handlebars = Handlebars.Create();
|
||||
|
||||
// TODO: Do we still need layouts with MJML?
|
||||
var assembly = typeof(HandlebarMailRenderer).Assembly;
|
||||
var layoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.Full.html.hbs");
|
||||
handlebars.RegisterTemplate("FullHtmlLayout", layoutSource);
|
||||
|
||||
return handlebars;
|
||||
}
|
||||
}
|
||||
7
src/Core/Platform/Mailer/IMailRenderer.cs
Normal file
7
src/Core/Platform/Mailer/IMailRenderer.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
#nullable enable
|
||||
namespace Bit.Core.Platform.Mailer;
|
||||
|
||||
public interface IMailRenderer
|
||||
{
|
||||
Task<(string html, string txt)> RenderAsync(BaseMailView model);
|
||||
}
|
||||
15
src/Core/Platform/Mailer/IMailer.cs
Normal file
15
src/Core/Platform/Mailer/IMailer.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Bit.Core.Platform.Mailer;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Generic mailer interface for sending email messages.
|
||||
/// </summary>
|
||||
public interface IMailer
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends an email message.
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public Task SendEmail<T>(BaseMail<T> message) where T : BaseMailView;
|
||||
}
|
||||
32
src/Core/Platform/Mailer/Mailer.cs
Normal file
32
src/Core/Platform/Mailer/Mailer.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Platform.Mailer;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public class Mailer(IMailRenderer renderer, IMailDeliveryService mailDeliveryService) : IMailer
|
||||
{
|
||||
public async Task SendEmail<T>(BaseMail<T> message) where T : BaseMailView
|
||||
{
|
||||
var content = await renderer.RenderAsync(message.View);
|
||||
|
||||
var metadata = new Dictionary<string, object>();
|
||||
if (message.IgnoreSuppressList)
|
||||
{
|
||||
metadata.Add("SendGridBypassListManagement", true);
|
||||
}
|
||||
|
||||
var mailMessage = new MailMessage
|
||||
{
|
||||
ToEmails = message.ToEmails,
|
||||
Subject = message.Subject,
|
||||
MetaData = metadata,
|
||||
HtmlContent = content.html,
|
||||
TextContent = content.txt,
|
||||
Category = message.Category,
|
||||
};
|
||||
|
||||
await mailDeliveryService.SendEmailAsync(mailMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Bit.Core.Platform.Mailer;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for adding the Mailer feature to the service collection.
|
||||
/// </summary>
|
||||
public static class MailerServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the Mailer services to the <see cref="IServiceCollection"/>.
|
||||
/// This includes the mail renderer and mailer for sending templated emails.
|
||||
/// This method is safe to be run multiple times.
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
|
||||
/// <returns>The <see cref="IServiceCollection"/> for additional chaining.</returns>
|
||||
public static IServiceCollection AddMailer(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<IMailRenderer, HandlebarMailRenderer>();
|
||||
services.TryAddSingleton<IMailer, Mailer>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
200
src/Core/Platform/Mailer/README.md
Normal file
200
src/Core/Platform/Mailer/README.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Mailer
|
||||
|
||||
The Mailer feature provides a structured, type-safe approach to sending emails in the Bitwarden server application. It
|
||||
uses Handlebars templates to render both HTML and plain text email content.
|
||||
|
||||
## Architecture
|
||||
|
||||
The Mailer system consists of four main components:
|
||||
|
||||
1. **IMailer** - Service interface for sending emails
|
||||
2. **BaseMail<TView>** - Abstract base class defining email metadata (recipients, subject, category)
|
||||
3. **BaseMailView** - Abstract base class for email template view models
|
||||
4. **IMailRenderer** - Internal interface for rendering templates (implemented by `HandlebarMailRenderer`)
|
||||
|
||||
## How To Use
|
||||
|
||||
1. Define a view model that inherits from `BaseMailView` with properties for template data
|
||||
2. Create Handlebars templates (`.html.hbs` and `.text.hbs`) as embedded resources, preferably using the MJML pipeline,
|
||||
`/src/Core/MailTemplates/Mjml`.
|
||||
3. Define an email class that inherits from `BaseMail<TView>` with metadata like subject
|
||||
4. Use `IMailer.SendEmail()` to render and send the email
|
||||
|
||||
## Creating a New Email
|
||||
|
||||
### Step 1: Define the Email & View Model
|
||||
|
||||
Create a class that inherits from `BaseMailView`:
|
||||
|
||||
```csharp
|
||||
using Bit.Core.Platform.Mailer;
|
||||
|
||||
namespace MyApp.Emails;
|
||||
|
||||
public class WelcomeEmailView : BaseMailView
|
||||
{
|
||||
public required string UserName { get; init; }
|
||||
public required string ActivationUrl { get; init; }
|
||||
}
|
||||
|
||||
public class WelcomeEmail : BaseMail<WelcomeEmailView>
|
||||
{
|
||||
public override string Subject => "Welcome to Bitwarden";
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Create Handlebars Templates
|
||||
|
||||
Create two template files as embedded resources next to your view model. **Important**: The file names must be located
|
||||
directly next to the `ViewClass` and match the name of the view.
|
||||
|
||||
**WelcomeEmailView.html.hbs** (HTML version):
|
||||
|
||||
```handlebars
|
||||
<h1>Welcome, {{ UserName }}!</h1>
|
||||
<p>Thank you for joining Bitwarden.</p>
|
||||
<p>
|
||||
<a href="{{ ActivationUrl }}">Activate your account</a>
|
||||
</p>
|
||||
<p><small>© {{ CurrentYear }} Bitwarden Inc.</small></p>
|
||||
```
|
||||
|
||||
**WelcomeEmailView.text.hbs** (plain text version):
|
||||
|
||||
```handlebars
|
||||
Welcome, {{ UserName }}!
|
||||
|
||||
Thank you for joining Bitwarden.
|
||||
|
||||
Activate your account: {{ ActivationUrl }}
|
||||
|
||||
<EFBFBD> {{ CurrentYear }} Bitwarden Inc.
|
||||
```
|
||||
|
||||
**Important**: Template files must be configured as embedded resources in your `.csproj`:
|
||||
|
||||
```xml
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="**\*.hbs" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
### Step 3: Send the Email
|
||||
|
||||
Inject `IMailer` and send the email, this may be done in a service, command or some other application layer.
|
||||
|
||||
```csharp
|
||||
public class SomeService
|
||||
{
|
||||
private readonly IMailer _mailer;
|
||||
|
||||
public SomeService(IMailer mailer)
|
||||
{
|
||||
_mailer = mailer;
|
||||
}
|
||||
|
||||
public async Task SendWelcomeEmailAsync(string email, string userName, string activationUrl)
|
||||
{
|
||||
var mail = new WelcomeEmail
|
||||
{
|
||||
ToEmails = [email],
|
||||
View = new WelcomeEmailView
|
||||
{
|
||||
UserName = userName,
|
||||
ActivationUrl = activationUrl
|
||||
}
|
||||
};
|
||||
|
||||
await _mailer.SendEmail(mail);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Multiple Recipients
|
||||
|
||||
Send to multiple recipients by providing multiple email addresses:
|
||||
|
||||
```csharp
|
||||
var mail = new WelcomeEmail
|
||||
{
|
||||
ToEmails = ["user1@example.com", "user2@example.com"],
|
||||
View = new WelcomeEmailView { /* ... */ }
|
||||
};
|
||||
```
|
||||
|
||||
### Bypass Suppression List
|
||||
|
||||
For critical emails like account recovery or email OTP, you can bypass the suppression list:
|
||||
|
||||
```csharp
|
||||
public class PasswordResetEmail : BaseMail<PasswordResetEmailView>
|
||||
{
|
||||
public override string Subject => "Reset Your Password";
|
||||
public override bool IgnoreSuppressList => true; // Use with caution
|
||||
}
|
||||
```
|
||||
|
||||
**Warning**: Only use `IgnoreSuppressList = true` for critical account recovery or authentication emails.
|
||||
|
||||
### Email Categories
|
||||
|
||||
Optionally categorize emails for processing at the upstream email delivery service:
|
||||
|
||||
```csharp
|
||||
public class MarketingEmail : BaseMail<MarketingEmailView>
|
||||
{
|
||||
public override string Subject => "Latest Updates";
|
||||
public string? Category => "marketing";
|
||||
}
|
||||
```
|
||||
|
||||
## Built-in View Properties
|
||||
|
||||
All view models inherit from `BaseMailView`, which provides:
|
||||
|
||||
- **CurrentYear** - The current UTC year (useful for copyright notices)
|
||||
|
||||
```handlebars
|
||||
|
||||
<footer>© {{ CurrentYear }} Bitwarden Inc.</footer>
|
||||
```
|
||||
|
||||
## Template Naming Convention
|
||||
|
||||
Templates must follow this naming convention:
|
||||
|
||||
- HTML template: `{ViewModelFullName}.html.hbs`
|
||||
- Text template: `{ViewModelFullName}.text.hbs`
|
||||
|
||||
For example, if your view model is `Bit.Core.Auth.Models.Mail.VerifyEmailView`, the templates must be:
|
||||
|
||||
- `Bit.Core.Auth.Models.Mail.VerifyEmailView.html.hbs`
|
||||
- `Bit.Core.Auth.Models.Mail.VerifyEmailView.text.hbs`
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
Register the Mailer services in your DI container using the extension method:
|
||||
|
||||
```csharp
|
||||
using Bit.Core.Platform.Mailer;
|
||||
|
||||
services.AddMailer();
|
||||
```
|
||||
|
||||
Or manually register the services:
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
services.TryAddSingleton<IMailRenderer, HandlebarMailRenderer>();
|
||||
services.TryAddSingleton<IMailer, Mailer>();
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- **Template caching** - `HandlebarMailRenderer` automatically caches compiled templates
|
||||
- **Lazy initialization** - Handlebars is initialized only when first needed
|
||||
- **Thread-safe** - The renderer is thread-safe for concurrent email rendering
|
||||
@@ -508,9 +508,15 @@
|
||||
<data name="UserIdAndTokenMismatch" xml:space="preserve">
|
||||
<value>Supplied userId and token did not match.</value>
|
||||
</data>
|
||||
<data name="UserShouldBeFound" xml:space="preserve">
|
||||
<value>User should have been defined by this point.</value>
|
||||
</data>
|
||||
<data name="CouldNotFindOrganization" xml:space="preserve">
|
||||
<value>Could not find organization for '{0}'</value>
|
||||
</data>
|
||||
<data name="CouldNotFindOrganizationUser" xml:space="preserve">
|
||||
<value>Could not find organization user for user '{0}' organization '{1}'</value>
|
||||
</data>
|
||||
<data name="NoSeatsAvailable" xml:space="preserve">
|
||||
<value>No seats available for organization, '{0}'</value>
|
||||
</data>
|
||||
|
||||
@@ -677,6 +677,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public bool Production { get; set; }
|
||||
public string Token { get; set; }
|
||||
public string NotificationUrl { get; set; }
|
||||
public string WebhookKey { get; set; }
|
||||
}
|
||||
|
||||
public class InstallationSettings : IInstallationSettings
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Settings;
|
||||
|
||||
namespace Bit.Core.Utilities;
|
||||
|
||||
public class BitPayClient
|
||||
{
|
||||
private readonly BitPayLight.BitPay _bpClient;
|
||||
|
||||
public BitPayClient(GlobalSettings globalSettings)
|
||||
{
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.BitPay.Token))
|
||||
{
|
||||
_bpClient = new BitPayLight.BitPay(globalSettings.BitPay.Token,
|
||||
globalSettings.BitPay.Production ? BitPayLight.Env.Prod : BitPayLight.Env.Test);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<BitPayLight.Models.Invoice.Invoice> GetInvoiceAsync(string id)
|
||||
{
|
||||
return _bpClient.GetInvoice(id);
|
||||
}
|
||||
|
||||
public Task<BitPayLight.Models.Invoice.Invoice> CreateInvoiceAsync(BitPayLight.Models.Invoice.Invoice invoice)
|
||||
{
|
||||
return _bpClient.CreateInvoice(invoice);
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,8 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
|
||||
UseAdminSponsoredFamilies = o.UseAdminSponsoredFamilies,
|
||||
LimitItemDeletion = o.LimitItemDeletion,
|
||||
IsAdminInitiated = os.IsAdminInitiated,
|
||||
UseOrganizationDomains = o.UseOrganizationDomains
|
||||
UseOrganizationDomains = o.UseOrganizationDomains,
|
||||
UseAutomaticUserConfirmation = o.UseAutomaticUserConfirmation
|
||||
};
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
|
||||
join po in dbContext.ProviderOrganizations on pu.ProviderId equals po.ProviderId
|
||||
join o in dbContext.Organizations on po.OrganizationId equals o.Id
|
||||
join p in dbContext.Providers on pu.ProviderId equals p.Id
|
||||
select new { pu, po, o, p };
|
||||
join ss in dbContext.SsoConfigs on o.Id equals ss.OrganizationId into ss_g
|
||||
from ss in ss_g.DefaultIfEmpty()
|
||||
select new { pu, po, o, p, ss };
|
||||
return query.Select(x => new ProviderUserOrganizationDetails
|
||||
{
|
||||
OrganizationId = x.po.OrganizationId,
|
||||
@@ -29,6 +31,9 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
|
||||
UseTotp = x.o.UseTotp,
|
||||
Use2fa = x.o.Use2fa,
|
||||
UseApi = x.o.UseApi,
|
||||
UseResetPassword = x.o.UseResetPassword,
|
||||
UseSecretsManager = x.o.UseSecretsManager,
|
||||
UsePasswordManager = x.o.UsePasswordManager,
|
||||
SelfHost = x.o.SelfHost,
|
||||
UsersGetPremium = x.o.UsersGetPremium,
|
||||
UseCustomPermissions = x.o.UseCustomPermissions,
|
||||
@@ -39,6 +44,7 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
|
||||
Key = x.po.Key,
|
||||
Status = x.pu.Status,
|
||||
Type = x.pu.Type,
|
||||
ProviderUserId = x.pu.Id,
|
||||
PublicKey = x.o.PublicKey,
|
||||
PrivateKey = x.o.PrivateKey,
|
||||
ProviderId = x.p.Id,
|
||||
@@ -52,7 +58,9 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
|
||||
ProviderType = x.p.Type,
|
||||
UseOrganizationDomains = x.o.UseOrganizationDomains,
|
||||
UseAdminSponsoredFamilies = x.o.UseAdminSponsoredFamilies,
|
||||
UseAutomaticUserConfirmation = x.o.UseAutomaticUserConfirmation
|
||||
UseAutomaticUserConfirmation = x.o.UseAutomaticUserConfirmation,
|
||||
SsoEnabled = x.ss.Enabled,
|
||||
SsoConfig = x.ss.Data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ using Bit.Core.KeyManagement;
|
||||
using Bit.Core.NotificationCenter;
|
||||
using Bit.Core.OrganizationFeatures;
|
||||
using Bit.Core.Platform;
|
||||
using Bit.Core.Platform.Mailer;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.PushRegistration.Internal;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -242,8 +243,11 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IPaymentService, StripePaymentService>();
|
||||
services.AddScoped<IPaymentHistoryService, PaymentHistoryService>();
|
||||
services.AddScoped<ITwoFactorEmailService, TwoFactorEmailService>();
|
||||
// Legacy mailer service
|
||||
services.AddSingleton<IStripeSyncService, StripeSyncService>();
|
||||
services.AddSingleton<IMailService, HandlebarsMailService>();
|
||||
// Modern mailers
|
||||
services.AddMailer();
|
||||
services.AddSingleton<ILicensingService, LicensingService>();
|
||||
services.AddSingleton<ILookupClient>(_ =>
|
||||
{
|
||||
|
||||
@@ -6,7 +6,19 @@ CREATE PROCEDURE [dbo].[OrganizationReport_Create]
|
||||
@ContentEncryptionKey VARCHAR(MAX),
|
||||
@SummaryData NVARCHAR(MAX),
|
||||
@ApplicationData NVARCHAR(MAX),
|
||||
@RevisionDate DATETIME2(7)
|
||||
@RevisionDate DATETIME2(7),
|
||||
@ApplicationCount INT = NULL,
|
||||
@ApplicationAtRiskCount INT = NULL,
|
||||
@CriticalApplicationCount INT = NULL,
|
||||
@CriticalApplicationAtRiskCount INT = NULL,
|
||||
@MemberCount INT = NULL,
|
||||
@MemberAtRiskCount INT = NULL,
|
||||
@CriticalMemberCount INT = NULL,
|
||||
@CriticalMemberAtRiskCount INT = NULL,
|
||||
@PasswordCount INT = NULL,
|
||||
@PasswordAtRiskCount INT = NULL,
|
||||
@CriticalPasswordCount INT = NULL,
|
||||
@CriticalPasswordAtRiskCount INT = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
@@ -20,7 +32,19 @@ INSERT INTO [dbo].[OrganizationReport](
|
||||
[ContentEncryptionKey],
|
||||
[SummaryData],
|
||||
[ApplicationData],
|
||||
[RevisionDate]
|
||||
[RevisionDate],
|
||||
[ApplicationCount],
|
||||
[ApplicationAtRiskCount],
|
||||
[CriticalApplicationCount],
|
||||
[CriticalApplicationAtRiskCount],
|
||||
[MemberCount],
|
||||
[MemberAtRiskCount],
|
||||
[CriticalMemberCount],
|
||||
[CriticalMemberAtRiskCount],
|
||||
[PasswordCount],
|
||||
[PasswordAtRiskCount],
|
||||
[CriticalPasswordCount],
|
||||
[CriticalPasswordAtRiskCount]
|
||||
)
|
||||
VALUES (
|
||||
@Id,
|
||||
@@ -30,6 +54,18 @@ VALUES (
|
||||
@ContentEncryptionKey,
|
||||
@SummaryData,
|
||||
@ApplicationData,
|
||||
@RevisionDate
|
||||
@RevisionDate,
|
||||
@ApplicationCount,
|
||||
@ApplicationAtRiskCount,
|
||||
@CriticalApplicationCount,
|
||||
@CriticalApplicationAtRiskCount,
|
||||
@MemberCount,
|
||||
@MemberAtRiskCount,
|
||||
@CriticalMemberCount,
|
||||
@CriticalMemberAtRiskCount,
|
||||
@PasswordCount,
|
||||
@PasswordAtRiskCount,
|
||||
@CriticalPasswordCount,
|
||||
@CriticalPasswordAtRiskCount
|
||||
);
|
||||
END
|
||||
|
||||
@@ -5,14 +5,7 @@ BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT TOP 1
|
||||
[Id],
|
||||
[OrganizationId],
|
||||
[ReportData],
|
||||
[CreationDate],
|
||||
[ContentEncryptionKey],
|
||||
[SummaryData],
|
||||
[ApplicationData],
|
||||
[RevisionDate]
|
||||
*
|
||||
FROM [dbo].[OrganizationReportView]
|
||||
WHERE [OrganizationId] = @OrganizationId
|
||||
ORDER BY [RevisionDate] DESC
|
||||
|
||||
@@ -6,7 +6,19 @@ CREATE PROCEDURE [dbo].[OrganizationReport_Update]
|
||||
@ContentEncryptionKey VARCHAR(MAX),
|
||||
@SummaryData NVARCHAR(MAX),
|
||||
@ApplicationData NVARCHAR(MAX),
|
||||
@RevisionDate DATETIME2(7)
|
||||
@RevisionDate DATETIME2(7),
|
||||
@ApplicationCount INT = NULL,
|
||||
@ApplicationAtRiskCount INT = NULL,
|
||||
@CriticalApplicationCount INT = NULL,
|
||||
@CriticalApplicationAtRiskCount INT = NULL,
|
||||
@MemberCount INT = NULL,
|
||||
@MemberAtRiskCount INT = NULL,
|
||||
@CriticalMemberCount INT = NULL,
|
||||
@CriticalMemberAtRiskCount INT = NULL,
|
||||
@PasswordCount INT = NULL,
|
||||
@PasswordAtRiskCount INT = NULL,
|
||||
@CriticalPasswordCount INT = NULL,
|
||||
@CriticalPasswordAtRiskCount INT = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
@@ -18,6 +30,18 @@ BEGIN
|
||||
[ContentEncryptionKey] = @ContentEncryptionKey,
|
||||
[SummaryData] = @SummaryData,
|
||||
[ApplicationData] = @ApplicationData,
|
||||
[RevisionDate] = @RevisionDate
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[ApplicationCount] = @ApplicationCount,
|
||||
[ApplicationAtRiskCount] = @ApplicationAtRiskCount,
|
||||
[CriticalApplicationCount] = @CriticalApplicationCount,
|
||||
[CriticalApplicationAtRiskCount] = @CriticalApplicationAtRiskCount,
|
||||
[MemberCount] = @MemberCount,
|
||||
[MemberAtRiskCount] = @MemberAtRiskCount,
|
||||
[CriticalMemberCount] = @CriticalMemberCount,
|
||||
[CriticalMemberAtRiskCount] = @CriticalMemberAtRiskCount,
|
||||
[PasswordCount] = @PasswordCount,
|
||||
[PasswordAtRiskCount] = @PasswordAtRiskCount,
|
||||
[CriticalPasswordCount] = @CriticalPasswordCount,
|
||||
[CriticalPasswordAtRiskCount] = @CriticalPasswordAtRiskCount
|
||||
WHERE [Id] = @Id;
|
||||
END;
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
CREATE TABLE [dbo].[OrganizationReport] (
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[ReportData] NVARCHAR(MAX) NOT NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
[ContentEncryptionKey] VARCHAR(MAX) NOT NULL,
|
||||
[SummaryData] NVARCHAR(MAX) NULL,
|
||||
[ApplicationData] NVARCHAR(MAX) NULL,
|
||||
[RevisionDate] DATETIME2 (7) NULL,
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[ReportData] NVARCHAR(MAX) NOT NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
[ContentEncryptionKey] VARCHAR(MAX) NOT NULL,
|
||||
[SummaryData] NVARCHAR(MAX) NULL,
|
||||
[ApplicationData] NVARCHAR(MAX) NULL,
|
||||
[RevisionDate] DATETIME2 (7) NULL,
|
||||
[ApplicationCount] INT NULL,
|
||||
[ApplicationAtRiskCount] INT NULL,
|
||||
[CriticalApplicationCount] INT NULL,
|
||||
[CriticalApplicationAtRiskCount] INT NULL,
|
||||
[MemberCount] INT NULL,
|
||||
[MemberAtRiskCount] INT NULL,
|
||||
[CriticalMemberCount] INT NULL,
|
||||
[CriticalMemberAtRiskCount] INT NULL,
|
||||
[PasswordCount] INT NULL,
|
||||
[PasswordAtRiskCount] INT NULL,
|
||||
[CriticalPasswordCount] INT NULL,
|
||||
[CriticalPasswordAtRiskCount] INT NULL,
|
||||
CONSTRAINT [PK_OrganizationReport] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_OrganizationReport_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])
|
||||
);
|
||||
|
||||
@@ -16,6 +16,8 @@ SELECT
|
||||
O.[Use2fa],
|
||||
O.[UseApi],
|
||||
O.[UseResetPassword],
|
||||
O.[UseSecretsManager],
|
||||
O.[UsePasswordManager],
|
||||
O.[SelfHost],
|
||||
O.[UsersGetPremium],
|
||||
O.[UseCustomPermissions],
|
||||
@@ -40,7 +42,9 @@ SELECT
|
||||
P.[Type] ProviderType,
|
||||
O.[LimitItemDeletion],
|
||||
O.[UseOrganizationDomains],
|
||||
O.[UseAutomaticUserConfirmation]
|
||||
O.[UseAutomaticUserConfirmation],
|
||||
SS.[Enabled] SsoEnabled,
|
||||
SS.[Data] SsoConfig
|
||||
FROM
|
||||
[dbo].[ProviderUser] PU
|
||||
INNER JOIN
|
||||
@@ -49,3 +53,5 @@ INNER JOIN
|
||||
[dbo].[Organization] O ON O.[Id] = PO.[OrganizationId]
|
||||
INNER JOIN
|
||||
[dbo].[Provider] P ON P.[Id] = PU.[ProviderId]
|
||||
LEFT JOIN
|
||||
[dbo].[SsoConfig] SS ON SS.[OrganizationId] = O.[Id]
|
||||
|
||||
Reference in New Issue
Block a user