mirror of https://github.com/zulip/zulip.git
remote_billing: Implement confirmation flow for RemoteRealm auth.
The way the flow goes now is this: 1. The user initiaties login via "Billing" in the gear menu. 2. That takes them to `/self-hosted-billing/` (possibly with a `next_page` param if we use that for some gear menu options). 3. The server queries the bouncer to give the user a link with a signed access token. 4. The user is redirected to that link (on `selfhosting.zulipchat.com`). Now we have two cases, either the user is logging in for the first time and already did in the past. If this is the first time, we have: 5. The user is asked to fill in their email in a form that's shown, pre-filled with the value provided inside the signed access token. They POST this to the next endpoint. 6. The next endpoint sends a confirmation email to that address and asks the user to go check their email. 7. The user clicks the link in their email is taken to the from_confirmation endpoint. 8. Their initial RemoteBillingUser is created, a new signed link like in (3) is generated and they're transparently taken back to (4), where now that they have a RemoteBillingUser, they're handled just like a user who already logged in before: If the user already logged in before, they go straight here: 9. "Confirm login" page - they're shown their information (email and full_name), can update their full name in the form if they want. They also accept ToS here if necessary. They POST this form back to the endpoint and finally have a logged in session. 10. They're redirected to billing (or `next_page`) now that they have access.
This commit is contained in:
parent
18ec4cd198
commit
423aebf98e
|
@ -31,7 +31,10 @@ from zerver.models import (
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings.ZILENCER_ENABLED:
|
if settings.ZILENCER_ENABLED:
|
||||||
from zilencer.models import PreregistrationRemoteServerBillingUser
|
from zilencer.models import (
|
||||||
|
PreregistrationRemoteRealmBillingUser,
|
||||||
|
PreregistrationRemoteServerBillingUser,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfirmationKeyError(Exception):
|
class ConfirmationKeyError(Exception):
|
||||||
|
@ -68,7 +71,9 @@ NoZilencerConfirmationObjT: TypeAlias = Union[
|
||||||
RealmReactivationStatus,
|
RealmReactivationStatus,
|
||||||
]
|
]
|
||||||
ZilencerConfirmationObjT: TypeAlias = Union[
|
ZilencerConfirmationObjT: TypeAlias = Union[
|
||||||
NoZilencerConfirmationObjT, "PreregistrationRemoteServerBillingUser"
|
NoZilencerConfirmationObjT,
|
||||||
|
"PreregistrationRemoteServerBillingUser",
|
||||||
|
"PreregistrationRemoteRealmBillingUser",
|
||||||
]
|
]
|
||||||
|
|
||||||
ConfirmationObjT = Union[NoZilencerConfirmationObjT, ZilencerConfirmationObjT]
|
ConfirmationObjT = Union[NoZilencerConfirmationObjT, ZilencerConfirmationObjT]
|
||||||
|
@ -197,6 +202,7 @@ class Confirmation(models.Model):
|
||||||
REALM_CREATION = 7
|
REALM_CREATION = 7
|
||||||
REALM_REACTIVATION = 8
|
REALM_REACTIVATION = 8
|
||||||
REMOTE_SERVER_BILLING_LEGACY_LOGIN = 9
|
REMOTE_SERVER_BILLING_LEGACY_LOGIN = 9
|
||||||
|
REMOTE_REALM_BILLING_LEGACY_LOGIN = 10
|
||||||
type = models.PositiveSmallIntegerField()
|
type = models.PositiveSmallIntegerField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -237,6 +243,9 @@ if settings.ZILENCER_ENABLED:
|
||||||
_properties[Confirmation.REMOTE_SERVER_BILLING_LEGACY_LOGIN] = ConfirmationType(
|
_properties[Confirmation.REMOTE_SERVER_BILLING_LEGACY_LOGIN] = ConfirmationType(
|
||||||
"remote_billing_legacy_server_from_login_confirmation_link"
|
"remote_billing_legacy_server_from_login_confirmation_link"
|
||||||
)
|
)
|
||||||
|
_properties[Confirmation.REMOTE_REALM_BILLING_LEGACY_LOGIN] = ConfirmationType(
|
||||||
|
"remote_realm_billing_from_login_confirmation_link"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def one_click_unsubscribe_link(user_profile: UserProfile, email_type: str) -> str:
|
def one_click_unsubscribe_link(user_profile: UserProfile, email_type: str) -> str:
|
||||||
|
|
|
@ -28,6 +28,7 @@ class RemoteBillingIdentityDict(TypedDict):
|
||||||
remote_server_uuid: str
|
remote_server_uuid: str
|
||||||
remote_realm_uuid: str
|
remote_realm_uuid: str
|
||||||
|
|
||||||
|
remote_billing_user_id: Optional[int]
|
||||||
authenticated_at: int
|
authenticated_at: int
|
||||||
uri_scheme: Literal["http://", "https://"]
|
uri_scheme: Literal["http://", "https://"]
|
||||||
|
|
||||||
|
@ -133,9 +134,8 @@ def get_remote_server_and_user_from_session(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
server_uuid: str,
|
server_uuid: str,
|
||||||
) -> Tuple[RemoteZulipServer, Optional[RemoteServerBillingUser]]:
|
) -> Tuple[RemoteZulipServer, Optional[RemoteServerBillingUser]]:
|
||||||
identity_dict = cast(
|
identity_dict: Optional[LegacyServerIdentityDict] = get_identity_dict_from_session(
|
||||||
Optional[LegacyServerIdentityDict],
|
request, realm_uuid=None, server_uuid=server_uuid
|
||||||
get_identity_dict_from_session(request, realm_uuid=None, server_uuid=server_uuid),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if identity_dict is None:
|
if identity_dict is None:
|
||||||
|
|
|
@ -33,6 +33,10 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
|
||||||
next_page: Optional[str] = None,
|
next_page: Optional[str] = None,
|
||||||
expect_tos: bool = True,
|
expect_tos: bool = True,
|
||||||
confirm_tos: bool = True,
|
confirm_tos: bool = True,
|
||||||
|
first_time_login: bool = True,
|
||||||
|
# This only matters if first_time_login is True, since otherwise
|
||||||
|
# there's no confirmation link to be clicked:
|
||||||
|
return_without_clicking_confirmation_link: bool = False,
|
||||||
) -> "TestHttpResponse":
|
) -> "TestHttpResponse":
|
||||||
now = timezone_now()
|
now = timezone_now()
|
||||||
|
|
||||||
|
@ -45,12 +49,61 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
self.assertIn("http://selfhosting.testserver/remote-billing-login/", result["Location"])
|
self.assertIn("http://selfhosting.testserver/remote-billing-login/", result["Location"])
|
||||||
|
|
||||||
# We've received a redirect to an URL that will grant us an authenticated
|
|
||||||
# session for remote billing.
|
|
||||||
signed_auth_url = result["Location"]
|
signed_auth_url = result["Location"]
|
||||||
|
signed_access_token = signed_auth_url.split("/")[-1]
|
||||||
with time_machine.travel(now, tick=False):
|
with time_machine.travel(now, tick=False):
|
||||||
result = self.client_get(signed_auth_url, subdomain="selfhosting")
|
result = self.client_get(signed_auth_url, subdomain="selfhosting")
|
||||||
# When successful, we see a confirmation page.
|
|
||||||
|
if first_time_login:
|
||||||
|
self.assertFalse(RemoteRealmBillingUser.objects.filter(user_uuid=user.uuid).exists())
|
||||||
|
# When logging in for the first time some extra steps are needed
|
||||||
|
# to confirm and verify the email address.
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
self.assert_in_success_response(["Enter your email address"], result)
|
||||||
|
self.assert_in_success_response([user.realm.host], result)
|
||||||
|
self.assert_in_success_response(
|
||||||
|
[f'action="/remote-billing-login/{signed_access_token}/confirm/"'], result
|
||||||
|
)
|
||||||
|
|
||||||
|
with time_machine.travel(now, tick=False):
|
||||||
|
result = self.client_post(
|
||||||
|
f"/remote-billing-login/{signed_access_token}/confirm/",
|
||||||
|
{"email": user.delivery_email},
|
||||||
|
subdomain="selfhosting",
|
||||||
|
)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
self.assert_in_success_response(
|
||||||
|
["To complete the login process, check your email account", user.delivery_email],
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
confirmation_url = self.get_confirmation_url_from_outbox(
|
||||||
|
user.delivery_email,
|
||||||
|
url_pattern=(
|
||||||
|
f"{settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN}.{settings.EXTERNAL_HOST}"
|
||||||
|
r"(\S+)>"
|
||||||
|
),
|
||||||
|
email_body_contains="Click the link below to complete the login process",
|
||||||
|
)
|
||||||
|
if return_without_clicking_confirmation_link:
|
||||||
|
return result
|
||||||
|
|
||||||
|
with time_machine.travel(now, tick=False):
|
||||||
|
result = self.client_get(confirmation_url, subdomain="selfhosting")
|
||||||
|
|
||||||
|
remote_billing_user = RemoteRealmBillingUser.objects.latest("id")
|
||||||
|
self.assertEqual(remote_billing_user.user_uuid, user.uuid)
|
||||||
|
self.assertEqual(remote_billing_user.email, user.delivery_email)
|
||||||
|
|
||||||
|
# Now we should be redirected again to the /remote-billing-login/ endpoint
|
||||||
|
# with a new signed_access_token. Now that the email has been confirmed,
|
||||||
|
# and we have a RemoteRealmBillingUser entry, we'll be in the same position
|
||||||
|
# as the case where first_time_login=False.
|
||||||
|
self.assertEqual(result.status_code, 302)
|
||||||
|
self.assertTrue(result["Location"].startswith("/remote-billing-login/"))
|
||||||
|
result = self.client_get(result["Location"], subdomain="selfhosting")
|
||||||
|
|
||||||
|
# Final confirmation page - just confirm your details, possibly
|
||||||
|
# agreeing to ToS if needed and an authenticated session will be granted:
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
self.assert_in_success_response(["Log in to Zulip server billing"], result)
|
self.assert_in_success_response(["Log in to Zulip server billing"], result)
|
||||||
self.assert_in_success_response([user.realm.host], result)
|
self.assert_in_success_response([user.realm.host], result)
|
||||||
|
@ -67,6 +120,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# Verify the authed data that should have been stored in the session.
|
# Verify the authed data that should have been stored in the session.
|
||||||
|
remote_billing_user = RemoteRealmBillingUser.objects.get(user_uuid=user.uuid)
|
||||||
identity_dict = RemoteBillingIdentityDict(
|
identity_dict = RemoteBillingIdentityDict(
|
||||||
user=RemoteBillingUserDict(
|
user=RemoteBillingUserDict(
|
||||||
user_email=user.delivery_email,
|
user_email=user.delivery_email,
|
||||||
|
@ -75,6 +129,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
|
||||||
),
|
),
|
||||||
remote_server_uuid=str(self.server.uuid),
|
remote_server_uuid=str(self.server.uuid),
|
||||||
remote_realm_uuid=str(user.realm.uuid),
|
remote_realm_uuid=str(user.realm.uuid),
|
||||||
|
remote_billing_user_id=remote_billing_user.id,
|
||||||
authenticated_at=datetime_to_timestamp(now),
|
authenticated_at=datetime_to_timestamp(now),
|
||||||
uri_scheme="http://",
|
uri_scheme="http://",
|
||||||
next_page=next_page,
|
next_page=next_page,
|
||||||
|
@ -189,6 +244,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
|
||||||
desdemona,
|
desdemona,
|
||||||
expect_tos=True,
|
expect_tos=True,
|
||||||
confirm_tos=False,
|
confirm_tos=False,
|
||||||
|
first_time_login=False,
|
||||||
)
|
)
|
||||||
self.assert_json_error(result, "You must accept the Terms of Service to proceed.")
|
self.assert_json_error(result, "You must accept the Terms of Service to proceed.")
|
||||||
|
|
||||||
|
@ -196,6 +252,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
|
||||||
desdemona,
|
desdemona,
|
||||||
expect_tos=True,
|
expect_tos=True,
|
||||||
confirm_tos=True,
|
confirm_tos=True,
|
||||||
|
first_time_login=False,
|
||||||
)
|
)
|
||||||
remote_billing_user.refresh_from_db()
|
remote_billing_user.refresh_from_db()
|
||||||
self.assertEqual(remote_billing_user.user_uuid, desdemona.uuid)
|
self.assertEqual(remote_billing_user.user_uuid, desdemona.uuid)
|
||||||
|
@ -252,6 +309,7 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
|
||||||
# ToS has already been confirmed earlier.
|
# ToS has already been confirmed earlier.
|
||||||
expect_tos=False,
|
expect_tos=False,
|
||||||
confirm_tos=False,
|
confirm_tos=False,
|
||||||
|
first_time_login=False,
|
||||||
)
|
)
|
||||||
self.assertEqual(result["Location"], f"/realm/{realm.uuid!s}/plans/")
|
self.assertEqual(result["Location"], f"/realm/{realm.uuid!s}/plans/")
|
||||||
result = self.client_get(result["Location"], subdomain="selfhosting")
|
result = self.client_get(result["Location"], subdomain="selfhosting")
|
||||||
|
@ -353,6 +411,28 @@ class RemoteBillingAuthenticationTest(BouncerTestCase):
|
||||||
["Upgrade", "Purchase Zulip", "Your subscription will renew automatically."], result
|
["Upgrade", "Purchase Zulip", "Your subscription will renew automatically."], result
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_remote_billing_authentication_flow_cant_access_billing_without_finishing_confirmation(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
self.login("desdemona")
|
||||||
|
desdemona = self.example_user("desdemona")
|
||||||
|
realm = desdemona.realm
|
||||||
|
|
||||||
|
self.add_mock_response()
|
||||||
|
|
||||||
|
result = self.execute_remote_billing_authentication_flow(
|
||||||
|
desdemona,
|
||||||
|
expect_tos=True,
|
||||||
|
confirm_tos=False,
|
||||||
|
first_time_login=True,
|
||||||
|
return_without_clicking_confirmation_link=True,
|
||||||
|
)
|
||||||
|
result = self.client_get(f"/realm/{realm.uuid!s}/billing/", subdomain="selfhosting")
|
||||||
|
# Access is not allowed. The user doesn't have an IdentityDict in the session, so
|
||||||
|
# we can't do a nice redirect back to their original server.
|
||||||
|
self.assertEqual(result.status_code, 401)
|
||||||
|
|
||||||
|
|
||||||
class LegacyServerLoginTest(BouncerTestCase):
|
class LegacyServerLoginTest(BouncerTestCase):
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -35,7 +35,9 @@ from corporate.views.remote_billing_page import (
|
||||||
remote_billing_legacy_server_confirm_login,
|
remote_billing_legacy_server_confirm_login,
|
||||||
remote_billing_legacy_server_from_login_confirmation_link,
|
remote_billing_legacy_server_from_login_confirmation_link,
|
||||||
remote_billing_legacy_server_login,
|
remote_billing_legacy_server_login,
|
||||||
|
remote_realm_billing_confirm_email,
|
||||||
remote_realm_billing_finalize_login,
|
remote_realm_billing_finalize_login,
|
||||||
|
remote_realm_billing_from_login_confirmation_link,
|
||||||
)
|
)
|
||||||
from corporate.views.session import (
|
from corporate.views.session import (
|
||||||
start_card_update_stripe_session,
|
start_card_update_stripe_session,
|
||||||
|
@ -186,6 +188,15 @@ urlpatterns = list(i18n_urlpatterns)
|
||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path("remote-billing-login/<signed_billing_access_token>", remote_realm_billing_finalize_login),
|
path("remote-billing-login/<signed_billing_access_token>", remote_realm_billing_finalize_login),
|
||||||
|
path(
|
||||||
|
"remote-billing-login/<signed_billing_access_token>/confirm/",
|
||||||
|
remote_realm_billing_confirm_email,
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"remote-billing-login/do_confirm/<confirmation_key>",
|
||||||
|
remote_realm_billing_from_login_confirmation_link,
|
||||||
|
name="remote_realm_billing_from_login_confirmation_link",
|
||||||
|
),
|
||||||
# Remote server billing endpoints.
|
# Remote server billing endpoints.
|
||||||
path("realm/<realm_uuid>/plans/", remote_realm_plans_page, name="remote_realm_plans_page"),
|
path("realm/<realm_uuid>/plans/", remote_realm_plans_page, name="remote_realm_plans_page"),
|
||||||
path(
|
path(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Literal, Optional
|
from typing import Any, Dict, Literal, Optional, Union, cast
|
||||||
from urllib.parse import urlsplit, urlunsplit
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -42,6 +42,7 @@ from zerver.lib.send_email import FromAddress, send_email
|
||||||
from zerver.lib.timestamp import datetime_to_timestamp
|
from zerver.lib.timestamp import datetime_to_timestamp
|
||||||
from zerver.lib.typed_endpoint import PathOnly, typed_endpoint
|
from zerver.lib.typed_endpoint import PathOnly, typed_endpoint
|
||||||
from zilencer.models import (
|
from zilencer.models import (
|
||||||
|
PreregistrationRemoteRealmBillingUser,
|
||||||
PreregistrationRemoteServerBillingUser,
|
PreregistrationRemoteServerBillingUser,
|
||||||
RemoteRealm,
|
RemoteRealm,
|
||||||
RemoteRealmBillingUser,
|
RemoteRealmBillingUser,
|
||||||
|
@ -86,6 +87,7 @@ def remote_realm_billing_entry(
|
||||||
),
|
),
|
||||||
remote_server_uuid=str(remote_server.uuid),
|
remote_server_uuid=str(remote_server.uuid),
|
||||||
remote_realm_uuid=str(remote_realm.uuid),
|
remote_realm_uuid=str(remote_realm.uuid),
|
||||||
|
remote_billing_user_id=None,
|
||||||
authenticated_at=datetime_to_timestamp(timezone_now()),
|
authenticated_at=datetime_to_timestamp(timezone_now()),
|
||||||
uri_scheme=uri_scheme,
|
uri_scheme=uri_scheme,
|
||||||
next_page=next_page,
|
next_page=next_page,
|
||||||
|
@ -100,14 +102,34 @@ def remote_realm_billing_entry(
|
||||||
return json_success(request, data={"billing_access_url": billing_access_url})
|
return json_success(request, data={"billing_access_url": billing_access_url})
|
||||||
|
|
||||||
|
|
||||||
|
def get_identity_dict_from_signed_access_token(
|
||||||
|
signed_billing_access_token: str,
|
||||||
|
) -> RemoteBillingIdentityDict:
|
||||||
|
try:
|
||||||
|
identity_dict: RemoteBillingIdentityDict = signing.loads(
|
||||||
|
signed_billing_access_token,
|
||||||
|
max_age=REMOTE_BILLING_SIGNED_ACCESS_TOKEN_VALIDITY_IN_SECONDS,
|
||||||
|
)
|
||||||
|
except signing.SignatureExpired:
|
||||||
|
raise JsonableError(_("Billing access token expired."))
|
||||||
|
except signing.BadSignature:
|
||||||
|
raise JsonableError(_("Invalid billing access token."))
|
||||||
|
return identity_dict
|
||||||
|
|
||||||
|
|
||||||
@self_hosting_management_endpoint
|
@self_hosting_management_endpoint
|
||||||
@typed_endpoint
|
@typed_endpoint
|
||||||
def remote_realm_billing_finalize_login(
|
def remote_realm_billing_finalize_login(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
*,
|
*,
|
||||||
signed_billing_access_token: PathOnly[str],
|
signed_billing_access_token: PathOnly[str],
|
||||||
|
full_name: Optional[str] = None,
|
||||||
tos_consent: Literal[None, "true"] = None,
|
tos_consent: Literal[None, "true"] = None,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
|
"""
|
||||||
|
This is the endpoint accessed via the billing_access_url, generated by
|
||||||
|
remote_realm_billing_entry entry.
|
||||||
|
"""
|
||||||
if request.method not in ["GET", "POST"]:
|
if request.method not in ["GET", "POST"]:
|
||||||
return HttpResponseNotAllowed(["GET", "POST"])
|
return HttpResponseNotAllowed(["GET", "POST"])
|
||||||
tos_consent_given = tos_consent == "true"
|
tos_consent_given = tos_consent == "true"
|
||||||
|
@ -118,23 +140,19 @@ def remote_realm_billing_finalize_login(
|
||||||
<= REMOTE_BILLING_SESSION_VALIDITY_SECONDS
|
<= REMOTE_BILLING_SESSION_VALIDITY_SECONDS
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
identity_dict = get_identity_dict_from_signed_access_token(signed_billing_access_token)
|
||||||
identity_dict: RemoteBillingIdentityDict = signing.loads(
|
|
||||||
signed_billing_access_token,
|
|
||||||
max_age=REMOTE_BILLING_SIGNED_ACCESS_TOKEN_VALIDITY_IN_SECONDS,
|
|
||||||
)
|
|
||||||
except signing.SignatureExpired:
|
|
||||||
raise JsonableError(_("Billing access token expired."))
|
|
||||||
except signing.BadSignature:
|
|
||||||
raise JsonableError(_("Invalid billing access token."))
|
|
||||||
|
|
||||||
# Now we want to fetch (or create) the RemoteRealmBillingUser object implied
|
# Now we want to fetch the RemoteRealmBillingUser object implied
|
||||||
# by the IdentityDict. We'll use this:
|
# by the IdentityDict. We'll use this:
|
||||||
# (1) If the user came here via just GET, we want to show them a confirmation
|
# (1) If the user came here via just GET, we want to show them a confirmation
|
||||||
# page with the relevant info details before finalizing login. If they wish
|
# page with the relevant info details before finalizing login. If they wish
|
||||||
# to proceed, they'll approve the form, causing a POST, bring us to case (2).
|
# to proceed, they'll approve the form, causing a POST, bring us to case (2).
|
||||||
# (2) If the user came here via POST, we finalize login, using the info from the
|
# (2) If the user came here via POST, we finalize login, using the info from the
|
||||||
# IdentityDict to update the RemoteRealmBillingUser object if needed.
|
# IdentityDict to update the RemoteRealmBillingUser object if needed.
|
||||||
|
# Finally, if the user is logging in for the first time, we'll need to create
|
||||||
|
# their account first. This will happen by making them fill out a form to confirm
|
||||||
|
# their email first. Only after clicking the confirmation link in the email,
|
||||||
|
# they will have their account created and finally be redirected back here.
|
||||||
remote_realm_uuid = identity_dict["remote_realm_uuid"]
|
remote_realm_uuid = identity_dict["remote_realm_uuid"]
|
||||||
remote_server_uuid = identity_dict["remote_server_uuid"]
|
remote_server_uuid = identity_dict["remote_server_uuid"]
|
||||||
try:
|
try:
|
||||||
|
@ -148,7 +166,6 @@ def remote_realm_billing_finalize_login(
|
||||||
user_dict = identity_dict["user"]
|
user_dict = identity_dict["user"]
|
||||||
|
|
||||||
user_email = user_dict["user_email"]
|
user_email = user_dict["user_email"]
|
||||||
user_full_name = user_dict["user_full_name"]
|
|
||||||
user_uuid = user_dict["user_uuid"]
|
user_uuid = user_dict["user_uuid"]
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
|
@ -164,16 +181,21 @@ def remote_realm_billing_finalize_login(
|
||||||
remote_user.tos_version.split(".")[0]
|
remote_user.tos_version.split(".")[0]
|
||||||
)
|
)
|
||||||
except RemoteRealmBillingUser.DoesNotExist:
|
except RemoteRealmBillingUser.DoesNotExist:
|
||||||
# This is the first time this user is logging in, so ToS consent needed.
|
# This is the first time this user is logging in.
|
||||||
|
remote_user = None
|
||||||
tos_consent_needed = True
|
tos_consent_needed = True
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
|
if remote_user is not None:
|
||||||
|
# Render a template where the user will just confirm their info,
|
||||||
|
# possibly accept ToS if needed, POST back here and will get fully
|
||||||
|
# authenticated.
|
||||||
context = {
|
context = {
|
||||||
"remote_server_uuid": remote_server_uuid,
|
"remote_server_uuid": remote_server_uuid,
|
||||||
"remote_realm_uuid": remote_realm_uuid,
|
"remote_realm_uuid": remote_realm_uuid,
|
||||||
"host": remote_realm.host,
|
"host": remote_realm.host,
|
||||||
"user_email": user_email,
|
"user_email": remote_user.email,
|
||||||
"user_full_name": user_full_name,
|
"user_full_name": remote_user.full_name,
|
||||||
"tos_consent_needed": tos_consent_needed,
|
"tos_consent_needed": tos_consent_needed,
|
||||||
"action_url": reverse(
|
"action_url": reverse(
|
||||||
remote_realm_billing_finalize_login, args=(signed_billing_access_token,)
|
remote_realm_billing_finalize_login, args=(signed_billing_access_token,)
|
||||||
|
@ -184,27 +206,46 @@ def remote_realm_billing_finalize_login(
|
||||||
"corporate/remote_realm_billing_finalize_login_confirmation.html",
|
"corporate/remote_realm_billing_finalize_login_confirmation.html",
|
||||||
context=context,
|
context=context,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# This user is logging in for the first time, so we need to create their
|
||||||
|
# RemoteRealmBillingUser object. Render a template where they'll
|
||||||
|
# enter their email address - we'll send a verification email to it.
|
||||||
|
context = {
|
||||||
|
"email": user_email,
|
||||||
|
"action_url": reverse(
|
||||||
|
remote_realm_billing_confirm_email, args=(signed_billing_access_token,)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
# TODO: We're re-using a template originally made for the legacy server
|
||||||
|
# flow. We should rename it and its HTML contents to a more general name.
|
||||||
|
"corporate/remote_billing_legacy_server_confirm_login_form.html",
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
assert request.method == "POST"
|
assert request.method == "POST"
|
||||||
|
if remote_user is None:
|
||||||
|
# Users logging in for the first time need to be created and follow
|
||||||
|
# a different path - they should not be POSTing here. It should be impossible
|
||||||
|
# to get here with a remote_user that is None without tampering with the form
|
||||||
|
# or manualling crafting a POST request.
|
||||||
|
raise JsonableError(_("User account doesn't exist yet."))
|
||||||
|
|
||||||
if tos_consent_needed and not tos_consent_given:
|
if tos_consent_needed and not tos_consent_given:
|
||||||
# This shouldn't be possible without tampering with the form, so we
|
# This shouldn't be possible without tampering with the form, so we
|
||||||
# don't need a pretty error.
|
# don't need a pretty error.
|
||||||
raise JsonableError(_("You must accept the Terms of Service to proceed."))
|
raise JsonableError(_("You must accept the Terms of Service to proceed."))
|
||||||
|
|
||||||
remote_user, created = RemoteRealmBillingUser.objects.get_or_create(
|
# The current approach is to update the full_name
|
||||||
defaults={"full_name": user_full_name, "email": user_email},
|
# based on what the user entered in the login confirmation form.
|
||||||
remote_realm=remote_realm,
|
# Usually they'll presumably just use the name already set for this object.
|
||||||
user_uuid=user_uuid,
|
if full_name is not None:
|
||||||
)
|
remote_user.full_name = full_name
|
||||||
|
|
||||||
# The current approach is to just update the email and full_name
|
|
||||||
# based on the info provided by the remote server during auth.
|
|
||||||
remote_user.email = user_email
|
|
||||||
remote_user.full_name = user_full_name
|
|
||||||
remote_user.tos_version = settings.TERMS_OF_SERVICE_VERSION
|
remote_user.tos_version = settings.TERMS_OF_SERVICE_VERSION
|
||||||
remote_user.save(update_fields=["email", "full_name", "tos_version"])
|
remote_user.save(update_fields=["full_name", "tos_version"])
|
||||||
|
|
||||||
|
identity_dict["remote_billing_user_id"] = remote_user.id
|
||||||
request.session["remote_billing_identities"] = {}
|
request.session["remote_billing_identities"] = {}
|
||||||
request.session["remote_billing_identities"][
|
request.session["remote_billing_identities"][
|
||||||
f"remote_realm:{remote_realm_uuid}"
|
f"remote_realm:{remote_realm_uuid}"
|
||||||
|
@ -224,6 +265,150 @@ def remote_realm_billing_finalize_login(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@self_hosting_management_endpoint
|
||||||
|
@typed_endpoint
|
||||||
|
def remote_realm_billing_confirm_email(
|
||||||
|
request: HttpRequest,
|
||||||
|
*,
|
||||||
|
signed_billing_access_token: PathOnly[str],
|
||||||
|
email: str,
|
||||||
|
) -> HttpResponse:
|
||||||
|
"""
|
||||||
|
Endpoint for users in the RemoteRealm flow that are logging in for the first time
|
||||||
|
and still have to have their RemoteRealmBillingUser object created.
|
||||||
|
Takes the POST from the above form asking for their email address
|
||||||
|
and sends confirmation email to the provided
|
||||||
|
email address in order to verify. Only the confirmation link will grant
|
||||||
|
a fully authenticated session.
|
||||||
|
"""
|
||||||
|
|
||||||
|
identity_dict = get_identity_dict_from_signed_access_token(signed_billing_access_token)
|
||||||
|
try:
|
||||||
|
remote_server = get_remote_server_by_uuid(identity_dict["remote_server_uuid"])
|
||||||
|
remote_realm = RemoteRealm.objects.get(
|
||||||
|
uuid=identity_dict["remote_realm_uuid"], server=remote_server
|
||||||
|
)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise AssertionError
|
||||||
|
|
||||||
|
obj = PreregistrationRemoteRealmBillingUser.objects.create(
|
||||||
|
email=email,
|
||||||
|
remote_realm=remote_realm,
|
||||||
|
user_uuid=identity_dict["user"]["user_uuid"],
|
||||||
|
next_page=identity_dict["next_page"],
|
||||||
|
uri_scheme=identity_dict["uri_scheme"],
|
||||||
|
)
|
||||||
|
url = create_remote_billing_confirmation_link(
|
||||||
|
obj,
|
||||||
|
Confirmation.REMOTE_REALM_BILLING_LEGACY_LOGIN,
|
||||||
|
# Use the same expiration time as for the signed access token,
|
||||||
|
# since this is similarly transient in nature.
|
||||||
|
validity_in_minutes=int(REMOTE_BILLING_SIGNED_ACCESS_TOKEN_VALIDITY_IN_SECONDS / 60),
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"remote_server_hostname": remote_server.hostname,
|
||||||
|
"remote_realm_host": remote_realm.host,
|
||||||
|
"confirmation_url": url,
|
||||||
|
}
|
||||||
|
send_email(
|
||||||
|
"zerver/emails/remote_realm_billing_confirm_login",
|
||||||
|
to_emails=[email],
|
||||||
|
from_address=FromAddress.tokenized_no_reply_address(),
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"corporate/remote_billing_email_confirmation_sent.html",
|
||||||
|
context={"email": email},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@self_hosting_management_endpoint
|
||||||
|
@typed_endpoint
|
||||||
|
def remote_realm_billing_from_login_confirmation_link(
|
||||||
|
request: HttpRequest,
|
||||||
|
*,
|
||||||
|
confirmation_key: PathOnly[str],
|
||||||
|
) -> HttpResponse:
|
||||||
|
"""
|
||||||
|
The user comes here via the confirmation link they received via email.
|
||||||
|
Creates the RemoteRealmBillingUser object and redirects to
|
||||||
|
remote_realm_billing_finalize_login with a new signed access token,
|
||||||
|
where they will finally be logged in now that they have an account.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
prereg_object = get_object_from_key(
|
||||||
|
confirmation_key,
|
||||||
|
[Confirmation.REMOTE_REALM_BILLING_LEGACY_LOGIN],
|
||||||
|
# These links aren't reusable. The user just clicks it
|
||||||
|
# to get their account created. Afterwards, they're not
|
||||||
|
# subject to the confirmation link part of the flow anymore.
|
||||||
|
mark_object_used=True,
|
||||||
|
)
|
||||||
|
except ConfirmationKeyError as exception:
|
||||||
|
return render_confirmation_key_error(request, exception)
|
||||||
|
assert isinstance(prereg_object, PreregistrationRemoteRealmBillingUser)
|
||||||
|
remote_realm = prereg_object.remote_realm
|
||||||
|
|
||||||
|
uri_scheme = prereg_object.uri_scheme
|
||||||
|
next_page = prereg_object.next_page
|
||||||
|
assert next_page in VALID_NEXT_PAGES
|
||||||
|
assert uri_scheme in ["http://", "https://"]
|
||||||
|
# Mypy is not satisfied by the above assert, so we need to cast.
|
||||||
|
uri_scheme = cast(Literal["http://", "https://"], uri_scheme)
|
||||||
|
|
||||||
|
remote_billing_user = RemoteRealmBillingUser.objects.create(
|
||||||
|
email=prereg_object.email,
|
||||||
|
remote_realm=remote_realm,
|
||||||
|
user_uuid=prereg_object.user_uuid,
|
||||||
|
)
|
||||||
|
|
||||||
|
identity_dict = RemoteBillingIdentityDict(
|
||||||
|
user=RemoteBillingUserDict(
|
||||||
|
user_email=remote_billing_user.email,
|
||||||
|
user_uuid=str(remote_billing_user.user_uuid),
|
||||||
|
user_full_name=remote_billing_user.full_name,
|
||||||
|
),
|
||||||
|
remote_server_uuid=str(remote_realm.server.uuid),
|
||||||
|
remote_realm_uuid=str(remote_realm.uuid),
|
||||||
|
# This will be figured out by the next endpoint in the flow anyway.
|
||||||
|
remote_billing_user_id=None,
|
||||||
|
authenticated_at=datetime_to_timestamp(timezone_now()),
|
||||||
|
uri_scheme=uri_scheme,
|
||||||
|
next_page=next_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
signed_identity_dict = signing.dumps(identity_dict)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
reverse(remote_realm_billing_finalize_login, args=[signed_identity_dict])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_remote_billing_confirmation_link(
|
||||||
|
obj: Union[PreregistrationRemoteRealmBillingUser, PreregistrationRemoteServerBillingUser],
|
||||||
|
confirmation_type: int,
|
||||||
|
validity_in_minutes: int,
|
||||||
|
) -> str:
|
||||||
|
url = create_confirmation_link(
|
||||||
|
obj,
|
||||||
|
confirmation_type,
|
||||||
|
validity_in_minutes=validity_in_minutes,
|
||||||
|
no_associated_realm_object=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# create_confirmation_link will create the url on the root subdomain, so we need to
|
||||||
|
# do a hacky approach to change it into the self hosting management subdomain.
|
||||||
|
new_hostname = f"{settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN}.{settings.EXTERNAL_HOST}"
|
||||||
|
split_url = urlsplit(url)
|
||||||
|
modified_url = split_url._replace(netloc=new_hostname)
|
||||||
|
final_url = urlunsplit(modified_url)
|
||||||
|
|
||||||
|
return final_url
|
||||||
|
|
||||||
|
|
||||||
def render_tmp_remote_billing_page(
|
def render_tmp_remote_billing_page(
|
||||||
request: HttpRequest, realm_uuid: Optional[str], server_uuid: Optional[str]
|
request: HttpRequest, realm_uuid: Optional[str], server_uuid: Optional[str]
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
|
@ -417,26 +602,18 @@ def remote_billing_legacy_server_confirm_login(
|
||||||
remote_server=remote_server,
|
remote_server=remote_server,
|
||||||
next_page=next_page,
|
next_page=next_page,
|
||||||
)
|
)
|
||||||
url = create_confirmation_link(
|
url = create_remote_billing_confirmation_link(
|
||||||
obj,
|
obj,
|
||||||
Confirmation.REMOTE_SERVER_BILLING_LEGACY_LOGIN,
|
Confirmation.REMOTE_SERVER_BILLING_LEGACY_LOGIN,
|
||||||
# Use the same expiration time as for the signed access token,
|
# Use the same expiration time as for the signed access token,
|
||||||
# since this is similarly transient in nature.
|
# since this is similarly transient in nature.
|
||||||
validity_in_minutes=int(REMOTE_BILLING_SIGNED_ACCESS_TOKEN_VALIDITY_IN_SECONDS / 60),
|
validity_in_minutes=int(REMOTE_BILLING_SIGNED_ACCESS_TOKEN_VALIDITY_IN_SECONDS / 60),
|
||||||
no_associated_realm_object=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# create_confirmation_link will create the url on the root subdomain, so we need to
|
|
||||||
# do a hacky approach to change it into the self hosting management subdomain.
|
|
||||||
new_hostname = f"{settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN}.{settings.EXTERNAL_HOST}"
|
|
||||||
split_url = urlsplit(url)
|
|
||||||
modified_url = split_url._replace(netloc=new_hostname)
|
|
||||||
final_url = urlunsplit(modified_url)
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"remote_server_hostname": remote_server.hostname,
|
"remote_server_hostname": remote_server.hostname,
|
||||||
"remote_server_uuid": str(remote_server.uuid),
|
"remote_server_uuid": str(remote_server.uuid),
|
||||||
"confirmation_url": final_url,
|
"confirmation_url": url,
|
||||||
}
|
}
|
||||||
send_email(
|
send_email(
|
||||||
"zerver/emails/remote_billing_legacy_server_confirm_login",
|
"zerver/emails/remote_billing_legacy_server_confirm_login",
|
||||||
|
@ -447,7 +624,7 @@ def remote_billing_legacy_server_confirm_login(
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"corporate/remote_billing_legacy_server_confirm_login_sent.html",
|
"corporate/remote_billing_email_confirmation_sent.html",
|
||||||
context={"email": email},
|
context={"email": email},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="input-box server-login-form-field">
|
<div class="input-box server-login-form-field">
|
||||||
<label for="email" class="inline-block label-title">Email</label>
|
<label for="email" class="inline-block label-title">Email</label>
|
||||||
<input id="email" name="email" class="email required" type="email" />
|
<input id="email" name="email" class="email required" type="email" {% if email %}value="{{ email }}"{% endif %} />
|
||||||
</div>
|
</div>
|
||||||
<div class="upgrade-button-container">
|
<div class="upgrade-button-container">
|
||||||
<button type="submit" id="server-confirm-login-button" class="stripe-button-el invoice-button">
|
<button type="submit" id="server-confirm-login-button" class="stripe-button-el invoice-button">
|
||||||
|
|
|
@ -14,18 +14,13 @@
|
||||||
<div class="white-box">
|
<div class="white-box">
|
||||||
<p>Click <b>Continue</b> to log in to Zulip server
|
<p>Click <b>Continue</b> to log in to Zulip server
|
||||||
billing with the account below.</p>
|
billing with the account below.</p>
|
||||||
{% if not legacy_server_confirmation_flow %}
|
|
||||||
Full name: {{ user_full_name }}<br />
|
|
||||||
{% endif %}
|
|
||||||
Email: {{ user_email }}<br />
|
Email: {{ user_email }}<br />
|
||||||
<form id="remote-realm-confirm-login-form" method="post" action="{{ action_url }}">
|
<form id="remote-realm-confirm-login-form" method="post" action="{{ action_url }}">
|
||||||
{{ csrf_input }}
|
{{ csrf_input }}
|
||||||
{% if legacy_server_confirmation_flow %}
|
|
||||||
<div class="input-box remote-realm-confirm-login-form-field">
|
<div class="input-box remote-realm-confirm-login-form-field">
|
||||||
<label for="full_name" class="inline-block label-title">Full name</label>
|
<label for="full_name" class="inline-block label-title">Full name</label>
|
||||||
<input id="full_name" name="full_name" class="required" type="text"/>
|
<input id="full_name" name="full_name" class="required" type="text" {% if user_full_name %}value="{{ user_full_name }}"{% endif %} />
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% if tos_consent_needed %}
|
{% if tos_consent_needed %}
|
||||||
<div class="input-group terms-of-service">
|
<div class="input-group terms-of-service">
|
||||||
<label for="id_terms" class="inline-block checkbox">
|
<label for="id_terms" class="inline-block checkbox">
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends "zerver/emails/email_base_default.html" %}
|
||||||
|
|
||||||
|
{% block illustration %}
|
||||||
|
<img src="{{ email_images_base_url }}/registration_confirmation.png" alt=""/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>
|
||||||
|
{{ _("You have initiated login to the Zulip plan management system for the following organization:") }}
|
||||||
|
<ul>
|
||||||
|
<li>{% trans %}Organization host: {{ remote_realm_host }}{% endtrans %}</li>
|
||||||
|
<li>{% trans %}Server host: {{ remote_server_hostname }}{% endtrans %}</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ _("Click the button below to complete the login process.") }}
|
||||||
|
<a class="button" href="{{ confirmation_url }}">{{ _("Confirm login") }}</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{macros.contact_us_zulip_cloud(support_email)}}
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% trans %}Confirm login to Zulip plan management{% endtrans %}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{{ _("You have initiated login to the Zulip plan management system for the following organization:") }}
|
||||||
|
* {% trans %}Organization host: {{ remote_realm_host }}{% endtrans %}
|
||||||
|
|
||||||
|
* {% trans %}Server host: {{ remote_server_hostname }}{% endtrans %}
|
||||||
|
|
||||||
|
{{ _("Click the link below to complete the login process;") }}
|
||||||
|
<{{ confirmation_url }}>
|
||||||
|
|
||||||
|
{% trans %}Do you have questions or feedback to share? Contact us at {{ support_email }} — we'd love to help!{% endtrans %}
|
|
@ -239,7 +239,11 @@ def check_banned_words(text: str) -> List[str]:
|
||||||
if word in lower_cased_text:
|
if word in lower_cased_text:
|
||||||
# Hack: Should move this into BANNED_WORDS framework; for
|
# Hack: Should move this into BANNED_WORDS framework; for
|
||||||
# now, just hand-code the skips:
|
# now, just hand-code the skips:
|
||||||
if "realm_name" in lower_cased_text or "realm_uri" in lower_cased_text:
|
if (
|
||||||
|
"realm_name" in lower_cased_text
|
||||||
|
or "realm_uri" in lower_cased_text
|
||||||
|
or "remote_realm_host" in lower_cased_text
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
kwargs = dict(word=word, text=text, reason=reason)
|
kwargs = dict(word=word, text=text, reason=reason)
|
||||||
msg = "{word} found in '{text}'. {reason}".format(**kwargs)
|
msg = "{word} found in '{text}'. {reason}".format(**kwargs)
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Generated by Django 4.2.8 on 2023-12-10 01:01
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("zilencer", "0048_remotezulipserver_last_api_feature_level"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="remoterealmbillinguser",
|
||||||
|
unique_together={("remote_realm", "user_uuid")},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PreregistrationRemoteRealmBillingUser",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("user_uuid", models.UUIDField()),
|
||||||
|
("email", models.EmailField(max_length=254)),
|
||||||
|
("status", models.IntegerField(default=0)),
|
||||||
|
("next_page", models.TextField(null=True)),
|
||||||
|
("uri_scheme", models.TextField()),
|
||||||
|
(
|
||||||
|
"remote_realm",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="zilencer.remoterealm"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -154,17 +154,40 @@ class RemoteRealm(models.Model):
|
||||||
return f"{self.host} {str(self.uuid)[0:12]}"
|
return f"{self.host} {str(self.uuid)[0:12]}"
|
||||||
|
|
||||||
|
|
||||||
class RemoteRealmBillingUser(models.Model):
|
class AbstractRemoteRealmBillingUser(models.Model):
|
||||||
remote_realm = models.ForeignKey(RemoteRealm, on_delete=models.CASCADE)
|
remote_realm = models.ForeignKey(RemoteRealm, on_delete=models.CASCADE)
|
||||||
|
|
||||||
# The .uuid of the UserProfile on the remote server
|
# The .uuid of the UserProfile on the remote server
|
||||||
user_uuid = models.UUIDField()
|
user_uuid = models.UUIDField()
|
||||||
full_name = models.TextField(default="")
|
|
||||||
email = models.EmailField()
|
email = models.EmailField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteRealmBillingUser(AbstractRemoteRealmBillingUser):
|
||||||
|
full_name = models.TextField(default="")
|
||||||
|
|
||||||
TOS_VERSION_BEFORE_FIRST_LOGIN = UserProfile.TOS_VERSION_BEFORE_FIRST_LOGIN
|
TOS_VERSION_BEFORE_FIRST_LOGIN = UserProfile.TOS_VERSION_BEFORE_FIRST_LOGIN
|
||||||
tos_version = models.TextField(default=TOS_VERSION_BEFORE_FIRST_LOGIN)
|
tos_version = models.TextField(default=TOS_VERSION_BEFORE_FIRST_LOGIN)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = [
|
||||||
|
("remote_realm", "user_uuid"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PreregistrationRemoteRealmBillingUser(AbstractRemoteRealmBillingUser):
|
||||||
|
# status: whether an object has been confirmed.
|
||||||
|
# if confirmed, set to confirmation.settings.STATUS_USED
|
||||||
|
status = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
# These are for carrying certain information that's originally
|
||||||
|
# in an IdentityDict across the confirmation link flow. These
|
||||||
|
# values will be restored in the final, fully authenticated IdentityDict.
|
||||||
|
next_page = models.TextField(null=True)
|
||||||
|
uri_scheme = models.TextField()
|
||||||
|
|
||||||
|
|
||||||
class AbstractRemoteServerBillingUser(models.Model):
|
class AbstractRemoteServerBillingUser(models.Model):
|
||||||
remote_server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE)
|
remote_server = models.ForeignKey(RemoteZulipServer, on_delete=models.CASCADE)
|
||||||
|
|
Loading…
Reference in New Issue