mirror of
https://github.com/bitwarden/server.git
synced 2025-07-19 08:30:59 -05:00
[PM-7004] Org Admin Initiate Delete (#3905)
* org delete * move org id to URL path * tweaks * lint fixes * Update src/Core/Services/Implementations/HandlebarsMailService.cs Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * Update src/Core/Services/Implementations/HandlebarsMailService.cs Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * PR feedback * fix id * [PM-7004] Move OrgDeleteTokenable to AdminConsole ownership * [PM-7004] Add consolidated billing logic into organization delete request acceptance endpoint * [PM-7004] Delete unused IOrganizationService.DeleteAsync(Organization organization, string token) method * [PM-7004] Fix unit tests * [PM-7004] Update delete organization request email templates * Add success message when initiating organization deletion * Refactor OrganizationsController request delete initiation action to handle exceptions --------- Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> Co-authored-by: Rui Tome <rtome@bitwarden.com>
This commit is contained in:
@ -269,6 +269,35 @@ public class OrganizationsController : Controller
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Org_Delete)]
|
||||
public async Task<IActionResult> DeleteInitiation(Guid id, OrganizationInitiateDeleteModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
TempData["Error"] = ModelState.GetErrorMessage();
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization != null)
|
||||
{
|
||||
await _organizationService.InitiateDeleteAsync(organization, model.AdminEmail);
|
||||
TempData["Success"] = "The request to initiate deletion of the organization has been sent.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData["Error"] = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
public async Task<IActionResult> TriggerBillingSync(Guid id)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
|
@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class OrganizationInitiateDeleteModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[StringLength(256)]
|
||||
[Display(Name = "Admin Email")]
|
||||
public string AdminEmail { get; set; }
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Admin.Models
|
||||
@using Bit.Core.Enums
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@ -18,24 +18,43 @@
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
document.getElementById('teams-trial').addEventListener('click', () => {
|
||||
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {
|
||||
alert('Organization is not on a free plan.');
|
||||
return;
|
||||
}
|
||||
setTrialDefaults('@((byte)PlanType.TeamsAnnually)');
|
||||
togglePlanFeatures('@((byte)PlanType.TeamsAnnually)');
|
||||
document.getElementById('@(nameof(Model.Plan))').value = 'Teams (Trial)';
|
||||
});
|
||||
document.getElementById('enterprise-trial').addEventListener('click', () => {
|
||||
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {
|
||||
alert('Organization is not on a free plan.');
|
||||
return;
|
||||
}
|
||||
setTrialDefaults('@((byte)PlanType.EnterpriseAnnually)');
|
||||
togglePlanFeatures('@((byte)PlanType.EnterpriseAnnually)');
|
||||
document.getElementById('@(nameof(Model.Plan))').value = 'Enterprise (Trial)';
|
||||
});
|
||||
const treamsTrialButton = document.getElementById('teams-trial');
|
||||
if (treamsTrialButton != null) {
|
||||
treamsTrialButton.addEventListener('click', () => {
|
||||
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {
|
||||
alert('Organization is not on a free plan.');
|
||||
return;
|
||||
}
|
||||
setTrialDefaults('@((byte)PlanType.TeamsAnnually)');
|
||||
togglePlanFeatures('@((byte)PlanType.TeamsAnnually)');
|
||||
document.getElementById('@(nameof(Model.Plan))').value = 'Teams (Trial)';
|
||||
});
|
||||
}
|
||||
|
||||
const entTrialButton = document.getElementById('enterprise-trial');
|
||||
if (entTrialButton != null) {
|
||||
entTrialButton.addEventListener('click', () => {
|
||||
if (document.getElementById('@(nameof(Model.PlanType))').value !== '@((byte)PlanType.Free)') {
|
||||
alert('Organization is not on a free plan.');
|
||||
return;
|
||||
}
|
||||
setTrialDefaults('@((byte)PlanType.EnterpriseAnnually)');
|
||||
togglePlanFeatures('@((byte)PlanType.EnterpriseAnnually)');
|
||||
document.getElementById('@(nameof(Model.Plan))').value = 'Enterprise (Trial)';
|
||||
});
|
||||
}
|
||||
|
||||
const initDeleteButton = document.getElementById('initiate-delete-form');
|
||||
if (initDeleteButton != null) {
|
||||
initDeleteButton.addEventListener('submit', (e) => {
|
||||
const email = prompt('Enter the email address of the owner/admin that your want to ' +
|
||||
'request the organization delete verification process with.');
|
||||
document.getElementById('AdminEmail').value = email;
|
||||
if (email == null || email === '') {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setTrialDefaults(planType) {
|
||||
// Plan
|
||||
@ -76,7 +95,7 @@
|
||||
{
|
||||
<h2>Billing Information</h2>
|
||||
@await Html.PartialAsync("_BillingInformation",
|
||||
new BillingInformationModel { BillingInfo = Model.BillingInfo, OrganizationId = Model.Organization.Id, Entity = "Organization" })
|
||||
new BillingInformationModel { BillingInfo = Model.BillingInfo, OrganizationId = Model.Organization.Id, Entity = "Organization" })
|
||||
}
|
||||
|
||||
@await Html.PartialAsync("~/AdminConsole/Views/Shared/_OrganizationForm.cshtml", Model)
|
||||
@ -95,18 +114,20 @@
|
||||
}
|
||||
@if (canUnlinkFromProvider && Model.Provider is not null)
|
||||
{
|
||||
<button
|
||||
class="btn btn-outline-danger mr-2"
|
||||
onclick="return unlinkProvider('@Model.Organization.Id');"
|
||||
>
|
||||
<button class="btn btn-outline-danger mr-2"
|
||||
onclick="return unlinkProvider('@Model.Organization.Id');">
|
||||
Unlink provider
|
||||
</button>
|
||||
}
|
||||
@if (canDelete)
|
||||
{
|
||||
<form asp-action="DeleteInitiation" asp-route-id="@Model.Organization.Id" id="initiate-delete-form">
|
||||
<input type="hidden" name="AdminEmail" id="AdminEmail" />
|
||||
<button class="btn btn-danger mr-2" type="submit">Request Delete</button>
|
||||
</form>
|
||||
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"
|
||||
onsubmit="return confirm('Are you sure you want to delete this organization?')">
|
||||
<button class="btn btn-danger" type="submit">Delete</button>
|
||||
onsubmit="return confirm('Are you sure you want to hard delete this organization?')">
|
||||
<button class="btn btn-outline-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user