From e515574b3efa7471ec589a2f581734227ee6a9b3 Mon Sep 17 00:00:00 2001 From: Mateusz Mandera Date: Tue, 12 Dec 2023 19:35:16 +0100 Subject: [PATCH] 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. --- corporate/lib/stripe.py | 3 ++ corporate/tests/test_remote_billing.py | 35 ++++++++++++++ corporate/urls.py | 6 +++ corporate/views/billing_page.py | 39 ++++++++++++++- corporate/views/remote_billing_page.py | 20 +++++++- .../remote_billing_server_deactivate.html | 47 +++++++++++++++++++ ...te_billing_server_deactivated_success.html | 27 +++++++++++ web/src/billing/deactivate_server.ts | 16 +++++++ web/webpack.assets.json | 1 + 9 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 templates/corporate/remote_billing_server_deactivate.html create mode 100644 templates/corporate/remote_billing_server_deactivated_success.html create mode 100644 web/src/billing/deactivate_server.ts diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 951bbcd753..d03d0862ef 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -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.", diff --git a/corporate/tests/test_remote_billing.py b/corporate/tests/test_remote_billing.py index 8f829f8f2e..d2c7397d88 100644 --- a/corporate/tests/test_remote_billing.py +++ b/corporate/tests/test_remote_billing.py @@ -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) diff --git a/corporate/urls.py b/corporate/urls.py index b927d10868..ad9cf24647 100644 --- a/corporate/urls.py +++ b/corporate/urls.py @@ -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//deactivate/", + remote_server_deactivate_page, + name="remote_server_deactivate_page", + ), path( "serverlogin/", remote_billing_legacy_server_login, diff --git a/corporate/views/billing_page.py b/corporate/views/billing_page.py index 460e62be69..c5bb1dc059 100644 --- a/corporate/views/billing_page.py +++ b/corporate/views/billing_page.py @@ -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}, + ) diff --git a/corporate/views/remote_billing_page.py b/corporate/views/remote_billing_page.py index 3fad10520f..25e6e66c4c 100644 --- a/corporate/views/remote_billing_page.py +++ b/corporate/views/remote_billing_page.py @@ -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 diff --git a/templates/corporate/remote_billing_server_deactivate.html b/templates/corporate/remote_billing_server_deactivate.html new file mode 100644 index 0000000000..6d81429f06 --- /dev/null +++ b/templates/corporate/remote_billing_server_deactivate.html @@ -0,0 +1,47 @@ +{% extends "zerver/portico.html" %} +{% set entrypoint = "upgrade" %} + +{% block title %} +{{ _("Deactivate server registration?") }} | Zulip +{% endblock %} + +{% block portico_content %} + +{% endblock %} diff --git a/templates/corporate/remote_billing_server_deactivated_success.html b/templates/corporate/remote_billing_server_deactivated_success.html new file mode 100644 index 0000000000..5f7a6f8166 --- /dev/null +++ b/templates/corporate/remote_billing_server_deactivated_success.html @@ -0,0 +1,27 @@ +{% extends "zerver/portico_signup.html" %} +{% set entrypoint = "upgrade" %} + +{% block title %} +Server registration deactivated | Zulip +{% endblock %} + +{% block portico_content %} +
+ +
+{% endblock %} + +{% block customhead %} +{{ super() }} +{% endblock %} diff --git a/web/src/billing/deactivate_server.ts b/web/src/billing/deactivate_server.ts new file mode 100644 index 0000000000..a5c96f798c --- /dev/null +++ b/web/src/billing/deactivate_server.ts @@ -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(); +}); diff --git a/web/webpack.assets.json b/web/webpack.assets.json index cd67a6a576..91e2115c92 100644 --- a/web/webpack.assets.json +++ b/web/webpack.assets.json @@ -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": [