remote_billing: Add flow for legacy servers.

This commit is contained in:
Mateusz Mandera
2023-11-29 21:30:46 +01:00
committed by Tim Abbott
parent d8cf12eaaa
commit 2765c63f56
7 changed files with 180 additions and 16 deletions

View File

@@ -18,6 +18,12 @@ class RemoteBillingIdentityDict(TypedDict):
remote_realm_uuid: str remote_realm_uuid: str
class LegacyServerIdentityDict(TypedDict):
# Currently this has only one field. We can extend this
# to add more information as appropriate.
remote_server_uuid: str
def get_identity_dict_from_session( def get_identity_dict_from_session(
request: HttpRequest, request: HttpRequest,
realm_uuid: Optional[str], realm_uuid: Optional[str],

View File

@@ -16,6 +16,7 @@ from corporate.views.portico import (
team_view, team_view,
) )
from corporate.views.remote_billing_page import ( from corporate.views.remote_billing_page import (
remote_billing_legacy_server_login,
remote_billing_page_realm, remote_billing_page_realm,
remote_billing_page_server, remote_billing_page_server,
remote_billing_plans_realm, remote_billing_plans_realm,
@@ -169,4 +170,9 @@ urlpatterns += [
path("realm/<realm_uuid>/billing", remote_billing_page_realm, name="remote_billing_page_realm"), path("realm/<realm_uuid>/billing", remote_billing_page_realm, name="remote_billing_page_realm"),
path("server/<server_uuid>/", remote_billing_page_server, name="remote_billing_page_server"), path("server/<server_uuid>/", remote_billing_page_server, name="remote_billing_page_server"),
path("realm/<realm_uuid>/upgrade", remote_realm_upgrade_page, name="remote_realm_upgrade_page"), path("realm/<realm_uuid>/upgrade", remote_realm_upgrade_page, name="remote_realm_upgrade_page"),
path(
"serverlogin/",
remote_billing_legacy_server_login,
name="remote_billing_legacy_server_login",
),
] ]

View File

@@ -6,12 +6,14 @@ from django.core import signing
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from django.utils.crypto import constant_time_compare
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from pydantic import Json from pydantic import Json
from corporate.lib.decorator import self_hosting_management_endpoint from corporate.lib.decorator import self_hosting_management_endpoint
from corporate.lib.remote_billing_util import ( from corporate.lib.remote_billing_util import (
LegacyServerIdentityDict,
RemoteBillingIdentityDict, RemoteBillingIdentityDict,
get_identity_dict_from_session, get_identity_dict_from_session,
) )
@@ -19,7 +21,7 @@ from zerver.lib.exceptions import JsonableError, MissingRemoteRealmError
from zerver.lib.remote_server import RealmDataForAnalytics, UserDataForRemoteBilling from zerver.lib.remote_server import RealmDataForAnalytics, UserDataForRemoteBilling
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import PathOnly, typed_endpoint from zerver.lib.typed_endpoint import PathOnly, typed_endpoint
from zilencer.models import RemoteRealm, RemoteZulipServer from zilencer.models import RemoteRealm, RemoteZulipServer, get_remote_server_by_uuid
billing_logger = logging.getLogger("corporate.stripe") billing_logger = logging.getLogger("corporate.stripe")
@@ -96,36 +98,46 @@ def render_tmp_remote_billing_page(
if identity_dict is None: if identity_dict is None:
raise JsonableError(_("User not authenticated")) raise JsonableError(_("User not authenticated"))
user_email = identity_dict["user_email"] # This key should be set in both RemoteRealm and legacy server
user_full_name = identity_dict["user_full_name"] # login flows.
remote_server_uuid = identity_dict["remote_server_uuid"] remote_server_uuid = identity_dict["remote_server_uuid"]
remote_realm_uuid = identity_dict["remote_realm_uuid"] user_email = identity_dict.get("user_email")
user_full_name = identity_dict.get("user_full_name")
remote_realm_uuid = identity_dict.get("remote_realm_uuid")
try: try:
remote_server = RemoteZulipServer.objects.get(uuid=remote_server_uuid) remote_server = RemoteZulipServer.objects.get(uuid=remote_server_uuid)
except RemoteZulipServer.DoesNotExist: except RemoteZulipServer.DoesNotExist:
raise JsonableError(_("Invalid remote server.")) raise JsonableError(_("Invalid remote server."))
try: remote_realm = None
# Checking for the (uuid, server) is sufficient to be secure here, since the server if remote_realm_uuid:
# is authenticated. the uuid_owner_secret is not needed here, it'll be used for try:
# for validating transfers of a realm to a different RemoteZulipServer (in the # Checking for the (uuid, server) is sufficient to be secure here, since the server
# export-import process). # is authenticated. the uuid_owner_secret is not needed here, it'll be used for
remote_realm = RemoteRealm.objects.get(uuid=remote_realm_uuid, server=remote_server) # for validating transfers of a realm to a different RemoteZulipServer (in the
except RemoteRealm.DoesNotExist: # export-import process).
raise AssertionError( remote_realm = RemoteRealm.objects.get(uuid=remote_realm_uuid, server=remote_server)
"The remote realm is missing despite being in the RemoteBillingIdentityDict" except RemoteRealm.DoesNotExist:
) raise AssertionError(
"The remote realm is missing despite being in the RemoteBillingIdentityDict"
)
remote_server_and_realm_info = { remote_server_and_realm_info = {
"remote_server_uuid": remote_server_uuid, "remote_server_uuid": remote_server_uuid,
"remote_server_hostname": remote_server.hostname, "remote_server_hostname": remote_server.hostname,
"remote_server_contact_email": remote_server.contact_email, "remote_server_contact_email": remote_server.contact_email,
"remote_server_plan_type": remote_server.plan_type, "remote_server_plan_type": remote_server.plan_type,
"remote_realm_uuid": remote_realm_uuid,
"remote_realm_host": remote_realm.host,
} }
if remote_realm is not None:
remote_server_and_realm_info.update(
{
"remote_realm_uuid": remote_realm_uuid,
"remote_realm_host": remote_realm.host,
}
)
return render( return render(
request, request,
"corporate/remote_billing.html", "corporate/remote_billing.html",
@@ -185,3 +197,54 @@ def remote_billing_page_server(request: HttpRequest, *, server_uuid: PathOnly[st
@typed_endpoint @typed_endpoint
def remote_billing_page_realm(request: HttpRequest, *, realm_uuid: PathOnly[str]) -> HttpResponse: def remote_billing_page_realm(request: HttpRequest, *, realm_uuid: PathOnly[str]) -> HttpResponse:
return remote_billing_page_common(request, realm_uuid=realm_uuid, server_uuid=None) return remote_billing_page_common(request, realm_uuid=realm_uuid, server_uuid=None)
@self_hosting_management_endpoint
@typed_endpoint
def remote_billing_legacy_server_login(
request: HttpRequest,
*,
server_org_id: Optional[str] = None,
server_org_secret: Optional[str] = None,
) -> HttpResponse:
if server_org_id is None or server_org_secret is None:
# Should not be possible to submit the form like this, so this is the default
# case, where the user just opened this page and therefore we render a fresh form.
return render(
request,
"corporate/legacy_server_login.html",
context={"error_message": False},
)
try:
remote_server = get_remote_server_by_uuid(server_org_id)
except RemoteZulipServer.DoesNotExist:
return render(
request,
"corporate/legacy_server_login.html",
context={
"error_message": _("Did not find a server registration for this server_org_id.")
},
)
if not constant_time_compare(server_org_secret, remote_server.api_key):
return render(
request,
"corporate/legacy_server_login.html",
context={"error_message": _("Invalid server_org_secret.")},
)
if remote_server.deactivated:
return render(
request,
"corporate/legacy_server_login.html",
context={"error_message": _("Your server registration has been deactivated.")},
)
remote_server_uuid = str(remote_server.uuid)
request.session["remote_billing_identities"] = {}
request.session["remote_billing_identities"][remote_server_uuid] = LegacyServerIdentityDict(
remote_server_uuid=remote_server_uuid
)
return HttpResponseRedirect(reverse("remote_billing_page_server", args=(remote_server_uuid,)))

View File

@@ -0,0 +1,45 @@
{% extends "zerver/portico.html" %}
{% set entrypoint = "upgrade" %}
{% set PAGE_TITLE = "Zulip server billing management" %}
{% block portico_content %}
<div class="register-account flex full-page server-login-page">
<div class="center-block new-style">
<div class="pitch">
<h1>Zulip server billing management</h1>
</div>
<div class="white-box">
{% if error_message %}
<div id="server-login-error" class="alert alert-danger">{{ error_message }}</div>
{% endif %}
<div id="server-login-input-section">
<form id="server-login-form" method="post" action="/serverlogin/">
{{ csrf_input }}
<div class="input-box server-login-form-field">
<label for="username" class="inline-block label-title">
server_org_id
<a href="https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html" target="_blank">
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
</label>
<input id="username" name="server_org_id" class="required" type="text"/>
</div>
<div class="input-box server-login-form-field">
<label for="password" class="inline-block label-title">server_org_key</label>
<input id="password" name="server_org_secret" class="required" type="password"/>
</div>
<div class="upgrade-button-container">
<button type="submit" id="server-login-button" class="stripe-button-el invoice-button">
<span class="server-login-button-text">Login</span>
<img class="loader server-login-button-loader" src="{{ static('images/loading/loader-white.svg') }}" alt="" />
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,29 @@
import $ from "jquery";
export function initialize(): void {
$("#server-login-form").validate({
errorClass: "text-error",
wrapper: "div",
submitHandler(form) {
$("#server-login-form").find(".loader").css("display", "inline-block");
$("#server-login-button .server-login-button-text").hide();
form.submit();
},
invalidHandler() {
// this removes all previous errors that were put on screen
// by the server.
$("#server-login-form .alert.alert-error").remove();
},
showErrors(error_map) {
if (error_map.password) {
$("#server-login-form .alert.alert-error").remove();
}
this.defaultShowErrors!();
},
});
}
$(() => {
initialize();
});

View File

@@ -656,3 +656,16 @@ input[name="licenses"] {
opacity: 0.8; opacity: 0.8;
font-weight: 400; font-weight: 400;
} }
.server-login-button-loader {
display: none;
vertical-align: top;
position: relative;
height: 30px;
margin-top: -10px;
top: 5px;
}
#server-login-error {
text-align: center;
}

View File

@@ -26,6 +26,8 @@
"./src/portico/tippyjs", "./src/portico/tippyjs",
"./src/billing/helpers", "./src/billing/helpers",
"./src/billing/upgrade", "./src/billing/upgrade",
"jquery-validation",
"./src/billing/legacy_server_login",
"./styles/portico/billing.css" "./styles/portico/billing.css"
], ],
"billing-event-status": [ "billing-event-status": [