remote_billing: Add endpoint and a helper to make deactivation links.

This is a general link for logging into the billing system on behalf of
a server, but it's tied to the .contact_email and takes the user
straight to the /deactivate/ page via the next_page mechanism.
This commit is contained in:
Mateusz Mandera 2023-12-12 19:35:16 +01:00 committed by Tim Abbott
parent fa9e3fb35c
commit e515574b3e
9 changed files with 190 additions and 4 deletions

View File

@ -3828,6 +3828,9 @@ def do_change_remote_server_plan_type(remote_server: RemoteZulipServer, plan_typ
@transaction.atomic
def do_deactivate_remote_server(remote_server: RemoteZulipServer) -> None:
# TODO: This should also ensure that the server doesn't have an active plan,
# and deactivate it otherwise. (Like do_deactivate_realm does.)
if remote_server.deactivated:
billing_logger.warning(
"Cannot deactivate remote server with ID %d, server has already been deactivated.",

View File

@ -15,6 +15,7 @@ from corporate.lib.remote_billing_util import (
RemoteBillingIdentityDict,
RemoteBillingUserDict,
)
from corporate.views.remote_billing_page import generate_confirmation_link_for_server_deactivation
from zerver.lib.remote_server import send_server_data_to_push_bouncer
from zerver.lib.test_classes import BouncerTestCase
from zerver.lib.timestamp import datetime_to_timestamp
@ -755,3 +756,37 @@ class LegacyServerLoginTest(BouncerTestCase):
)
self.assert_json_error(result, "You must accept the Terms of Service to proceed.")
class TestGenerateDeactivationLink(BouncerTestCase):
def test_generate_deactivation_link(self) -> None:
server = self.server
confirmation_url = generate_confirmation_link_for_server_deactivation(
server, validity_in_minutes=60
)
result = self.client_get(confirmation_url, subdomain="selfhosting")
self.assert_in_success_response(
["Log in to Zulip plan management", server.contact_email], result
)
payload = {"full_name": "test", "tos_consent": "true"}
result = self.client_post(confirmation_url, payload, subdomain="selfhosting")
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], f"/server/{server.uuid!s}/deactivate/")
result = self.client_get(result["Location"], subdomain="selfhosting")
self.assert_in_success_response(
[
"You are about to deactivate this server's",
server.hostname,
f'action="/server/{server.uuid!s}/deactivate/"',
],
result,
)
result = self.client_post(
f"/server/{server.uuid!s}/deactivate/", {"confirmed": "true"}, subdomain="selfhosting"
)
self.assert_in_success_response([f"Registration deactivated for {server.hostname}"], result)
server.refresh_from_db()
self.assertEqual(server.deactivated, True)

View File

@ -8,6 +8,7 @@ from corporate.views.billing_page import (
billing_page,
remote_realm_billing_page,
remote_server_billing_page,
remote_server_deactivate_page,
update_plan,
update_plan_for_remote_realm,
update_plan_for_remote_server,
@ -230,6 +231,11 @@ urlpatterns += [
remote_server_sponsorship_page,
name="remote_server_sponsorship_page",
),
path(
"server/<server_uuid>/deactivate/",
remote_server_deactivate_page,
name="remote_server_deactivate_page",
),
path(
"serverlogin/",
remote_billing_legacy_server_login,

View File

@ -1,9 +1,10 @@
import logging
from typing import Any, Dict, Optional
from typing import Any, Dict, Literal, Optional
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from django.utils.translation import gettext as _
from corporate.lib.decorator import (
authenticated_remote_realm_management_endpoint,
@ -14,9 +15,11 @@ from corporate.lib.stripe import (
RemoteRealmBillingSession,
RemoteServerBillingSession,
UpdatePlanRequest,
do_deactivate_remote_server,
)
from corporate.models import CustomerPlan, get_current_plan_by_customer, get_customer_by_realm
from zerver.decorator import process_as_post, require_billing_access, zulip_login_required
from zerver.lib.exceptions import JsonableError
from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import typed_endpoint
@ -274,3 +277,35 @@ def update_plan_for_remote_server(
)
billing_session.do_update_plan(update_plan_request)
return json_success(request)
@authenticated_remote_server_management_endpoint
@typed_endpoint
def remote_server_deactivate_page(
request: HttpRequest,
billing_session: RemoteServerBillingSession,
*,
confirmed: Literal[None, "true"] = None,
) -> HttpResponse:
if request.method not in ["GET", "POST"]: # nocoverage
return HttpResponseNotAllowed(["GET", "POST"])
remote_server = billing_session.remote_server
if request.method == "GET":
context = {
"server_hostname": remote_server.hostname,
"action_url": reverse(remote_server_deactivate_page, args=[str(remote_server.uuid)]),
}
return render(request, "corporate/remote_billing_server_deactivate.html", context=context)
assert request.method == "POST"
if confirmed is None: # nocoverage
# Should be impossible if the user is using the UI.
raise JsonableError(_("Parameter 'confirmed' is required"))
do_deactivate_remote_server(remote_server)
return render(
request,
"corporate/remote_billing_server_deactivated_success.html",
context={"server_hostname": remote_server.hostname},
)

View File

@ -54,8 +54,8 @@ from zilencer.models import (
billing_logger = logging.getLogger("corporate.stripe")
VALID_NEXT_PAGES = [None, "sponsorship", "upgrade", "billing", "plans"]
VALID_NEXT_PAGES_TYPE = Literal[None, "sponsorship", "upgrade", "billing", "plans"]
VALID_NEXT_PAGES = [None, "sponsorship", "upgrade", "billing", "plans", "deactivate"]
VALID_NEXT_PAGES_TYPE = Literal[None, "sponsorship", "upgrade", "billing", "plans", "deactivate"]
REMOTE_BILLING_SIGNED_ACCESS_TOKEN_VALIDITY_IN_SECONDS = 2 * 60 * 60
# We use units of hours here so that we can pass this through to the
@ -654,3 +654,19 @@ def remote_billing_legacy_server_from_login_confirmation_link(
return HttpResponseRedirect(
reverse("remote_server_billing_page", args=(remote_server_uuid,))
)
def generate_confirmation_link_for_server_deactivation(
remote_server: RemoteZulipServer, validity_in_minutes: int
) -> str:
obj = PreregistrationRemoteServerBillingUser.objects.create(
email=remote_server.contact_email,
remote_server=remote_server,
next_page="deactivate",
)
url = create_remote_billing_confirmation_link(
obj,
Confirmation.REMOTE_SERVER_BILLING_LEGACY_LOGIN,
validity_in_minutes=validity_in_minutes,
)
return url

View File

@ -0,0 +1,47 @@
{% extends "zerver/portico.html" %}
{% set entrypoint = "upgrade" %}
{% block title %}
<title>{{ _("Deactivate server registration?") }} | Zulip</title>
{% endblock %}
{% block portico_content %}
<div id="server-deactivate-page" class="register-account flex full-page">
<div class="center-block new-style">
<div class="pitch">
<h1>
Deactivate registration for {{ server_hostname }}?
</h1>
</div>
<div class="white-box">
<div id="server-deactivate-details">
<form id="server-deactivate-form" method="post" action="{{ action_url }}">
{{ csrf_input }}
<p class="not-editable-realm-field">
You are about to deactivate this server's
registration with
the <a href="https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html">Zulip
Mobile Push Notification Service</a>. This
will disable delivery of mobile push
notifications for all organizations hosted
on <b>{{ server_hostname }}</b>.
</p>
<input type="hidden" name="confirmed" value="true" />
<div class="upgrade-button-container">
<button type="submit" id="server-deactivate-button" class="stripe-button-el invoice-button">
<span class="server-deactivate-button-text">Deactivate registration</span>
<img class="loader remote-billing-button-loader" src="{{ static('images/loading/loader-white.svg') }}" alt="" />
</button>
</div>
</form>
<div class="input-box upgrade-page-field">
<div class="support-link not-editable-realm-field">
Questions? Contact <a href="mailto:support@zulip.com">support@zulip.com</a>.
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "zerver/portico_signup.html" %}
{% set entrypoint = "upgrade" %}
{% block title %}
<title>Server registration deactivated | Zulip</title>
{% endblock %}
{% block portico_content %}
<div class="app portico-page">
<div class="app-main portico-page-container center-block flex full-page account-creation account-email-confirm-container new-style">
<div class="pitch">
<h1>
Registration deactivated for {{ server_hostname }}
</h1>
</div>
<div class="inline-block">
<div class="white-box">
<p>Your server's registration has been deactivated.</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customhead %}
{{ super() }}
{% endblock %}

View File

@ -0,0 +1,16 @@
import $ from "jquery";
export function initialize(): void {
$("#server-deactivate-form").validate({
submitHandler(form) {
$("#server-deactivate-form").find(".loader").css("display", "inline-block");
$("#server-deactivate-button .server-deactivate-button-text").hide();
form.submit();
},
});
}
$(() => {
initialize();
});

View File

@ -28,6 +28,7 @@
"./src/billing/upgrade",
"jquery-validation",
"./src/billing/remote_billing_auth",
"./src/billing/deactivate_server",
"./styles/portico/billing.css"
],
"billing-event-status": [