registration: improve sign up flow

This adds separate paths for contributors and maintainers, the former
which skips the billing setup, and the latter which better explains the
payment requirements.
This commit is contained in:
Drew DeVault 2021-06-10 14:25:31 -04:00
parent fec992dd72
commit 72292457cb
10 changed files with 324 additions and 174 deletions

View File

@ -91,13 +91,14 @@ def index():
def register(): def register():
if current_user: if current_user:
return redirect("/") return redirect("/")
if cfg("meta.sr.ht::billing", "enabled") != "yes":
return redirect(url_for("auth.register_step2_GET"))
return render_template("register.html", site_key=site_key_id) return render_template("register.html", site_key=site_key_id)
@auth.route("/register/<invite_hash>") @auth.route("/register/<invite_hash>")
def register_invite(invite_hash): def register_invite(invite_hash):
if current_user: if current_user:
return redirect("/") return redirect("/")
if is_external_auth(): if is_external_auth():
return render_template("register.html") return render_template("register.html")
@ -110,13 +111,62 @@ def register_invite(invite_hash):
return render_template("register.html", site_key=site_key_id, return render_template("register.html", site_key=site_key_id,
invite_hash=invite_hash) invite_hash=invite_hash)
@csrf_bypass # for registration via sourcehut.org
@auth.route("/register", methods=["POST"]) @auth.route("/register", methods=["POST"])
def register_POST(): def register_POST():
valid = Validation(request)
is_open = allow_registration() is_open = allow_registration()
valid = Validation(request)
payment = valid.require("payment")
invite_hash = valid.optional("invite_hash")
if not valid.ok:
abort(400)
payment = payment == "yes"
if not is_open:
if not invite_hash:
abort(401)
else:
invite = (Invite.query
.filter(Invite.invite_hash == invite_hash)
.filter(Invite.recipient_id == None)
).one_or_none()
if not invite:
abort(401)
if invite_hash:
session["invite_hash"] = invite_hash
session["payment"] = payment
return redirect(url_for("auth.register_step2_GET"))
@auth.route("/register/step2")
def register_step2_GET():
invite_hash = session.get("invite_hash")
payment = session.get("payment", "no")
if current_user:
return redirect("/")
if invite_hash:
invite = (Invite.query
.filter(Invite.invite_hash == invite_hash)
.filter(Invite.recipient_id == None)
).one_or_none()
if not invite:
abort(404)
return render_template("register-step2.html",
site_key=site_key_id, invite_hash=invite_hash, payment=payment)
@csrf_bypass # for registration via sourcehut.org
@auth.route("/register/step2", methods=["POST"])
def register_step2_POST():
if current_user:
abort(400)
is_open = allow_registration()
session.pop("invite_hash", None)
payment = session.get("payment", False)
valid = Validation(request)
username = valid.require("username", friendly_name="Username") username = valid.require("username", friendly_name="Username")
email = valid.require("email", friendly_name="Email address") email = valid.require("email", friendly_name="Email address")
password = valid.require("password", friendly_name="Password") password = valid.require("password", friendly_name="Password")
@ -141,10 +191,9 @@ def register_POST():
valid.expect(False, "Invalid email address", field="email") valid.expect(False, "Invalid email address", field="email")
if not valid.ok: if not valid.ok:
return render_template("register.html", return render_template("register-step2.html",
is_open=(is_open or invite_hash is not None), is_open=(is_open or invite_hash is not None),
site_key=site_key_id, site_key=site_key_id, **valid.kwargs), 400
**valid.kwargs), 400
if is_abuse(valid): if is_abuse(valid):
return redirect("/registered") return redirect("/registered")
@ -167,17 +216,15 @@ def register_POST():
validate_password(valid, password) validate_password(valid, password)
if not valid.ok: if not valid.ok:
return render_template("register.html", return render_template("register-step2.html",
site_key=site_key_id,
is_open=(is_open or invite_hash is not None), is_open=(is_open or invite_hash is not None),
**valid.kwargs), 400 site_key=site_key_id, **valid.kwargs), 400
allow_plus_in_email = valid.optional("allow-plus-in-email") allow_plus_in_email = valid.optional("allow-plus-in-email")
if "+" in email and allow_plus_in_email != "yes": if "+" in email and allow_plus_in_email != "yes":
return render_template("register.html", return render_template("register-step2.html",
site_key=site_key_id,
is_open=(is_open or invite_hash is not None), is_open=(is_open or invite_hash is not None),
**valid.kwargs), 400 site_key=site_key_id, **valid.kwargs), 400
user = User(username) user = User(username)
user.email = email user.email = email
@ -248,11 +295,15 @@ def confirm_account(token):
audit_log("account confirmed", user=user) audit_log("account confirmed", user=user)
db.session.commit() db.session.commit()
login_user(user, set_cookie=True) login_user(user, set_cookie=True)
if cfg("meta.sr.ht::billing", "enabled") == "yes":
return redirect(url_for("billing.billing_initial_GET"))
metrics.meta_confirmations.inc() metrics.meta_confirmations.inc()
print(f"Confirmed account: {user.username} ({user.email})") print(f"Confirmed account: {user.username} ({user.email})")
return redirect(onboarding_redirect)
payment = session.pop("payment", False)
if payment and cfg("meta.sr.ht::billing", "enabled") == "yes":
return redirect(url_for("billing.billing_initial_GET"))
else:
return redirect(onboarding_redirect)
@auth.route("/login") @auth.route("/login")
def login_GET(): def login_GET():

View File

@ -14,43 +14,29 @@
<div class="col-md-12"> <div class="col-md-12">
<p> <p>
On {{cfg("sr.ht", "site-name")}}, all plans have access to the same On {{cfg("sr.ht", "site-name")}}, all plans have access to the same
features. You should pick the plan which best matches your financial needs features and in the same quantity. You should pick the plan which best
and best represents the level of investment you have in {{cfg("sr.ht", matches your financial needs and best represents the level of investment
"site-name")}}. If you require financial aid to use {{cfg("sr.ht", you have in {{cfg("sr.ht", "site-name")}}. If you require financial aid
"site-name")}}, please <a href="mailto:{{cfg("sr.ht", to use {{cfg("sr.ht", "site-name")}}, please
"owner-email")}}">send an email</a> explaining your circumstances and <a href="mailto:{{cfg("sr.ht", "owner-email")}}">send us an email</a>
we'll do our best to accommodate your needs. explaining your circumstances and we'll do our best to accommodate your
needs.
</p> </p>
<div class="alert alert-info"> <div class="alert alert-danger">
<strong>Notice</strong>: sr.ht is currently in <strong>alpha</strong>, <strong>Notice</strong>: {{cfg("sr.ht", "site-name")}} is currently
and the quality of the service may reflect that. As such, payment is considered at an alpha stage of development, and the quality of the
currently optional, and only encouraged for users who want to support the service may reflect that. However, the service is reliable, stable,
ongoing development of the site. For a summary of the guarantees and secure, and mostly complete at this stage of development. To learn
limitations that the alpha entails, see <a exactly what the alpha entails,
href="https://sourcehut.org/alpha-details/"
>this reference</a>. You may
<a <a
href="{{cfg("meta.sr.ht::settings", "onboarding-redirect")}}" href="https://sourcehut.org/alpha-details/"
>click here to continue without payment</a>. rel="noopener"
</div> target="_blank"
<div style="margin-bottom: 2rem"> >consult this document</a>.
<div class="progress"> During the alpha, payment is encouraged, but optional, for most features.
<div class="progress-bar" <a
role="progressbar" href="{{url_for("profile.profile_GET")}}"
style="width: {{paid_pct}}%;" >Continue without payment {{icon('caret-right')}}</a>.
aria-valuenow="{{paid_pct}}"
aria-valuemin="0" aria-valuemax="100"
>{{paid_pct}}% paid</div>
<div
class="goal"
style="left: 13.37%"
title="Presented without comment"
>13.37%</div>
<div class="progress-bar total">of {{total_users}} registered users</div>
</div>
<small class="text-muted pull-right">
Current number of paid accounts on {{cfg("sr.ht", "site-name")}}
</small>
</div> </div>
</div> </div>
</div> </div>
@ -104,7 +90,7 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="alert alert-warning"> <div class="alert alert-warning">
<strong>Notice</strong>: continuing to the next page will execute <strong>Notice</strong>: Continuing to the next page will execute
non-free JavaScript from our payment processor, non-free JavaScript from our payment processor,
<a <a
href="https://stripe.com/" href="https://stripe.com/"

View File

@ -1,4 +0,0 @@
<p>
{{cfg("sr.ht", "site-name")}} is a community and a network of websites
supporting hackers and their projects.
</p>

View File

@ -4,7 +4,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8 offset-md-2">
<h3> <h3>
Reset password Reset password
</h3> </h3>

View File

@ -4,17 +4,17 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8 offset-md-2">
<h3> <h3>
Log in to {{cfg("sr.ht", "site-name")}} Log in to {{cfg("sr.ht", "site-name")}}
<small> <small>
or <a href="/register">register</a> or <a href="/register">register</a>
</small> </small>
</h3> </h3>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6 offset-md-3">
<form method="POST" action="/login"> <form method="POST" action="/login">
{{csrf_token()}} {{csrf_token()}}
<div class="form-group"> <div class="form-group">

View File

@ -0,0 +1,156 @@
{% extends "layout.html" %}
{% block title %}
<title>Register for {{cfg("sr.ht", "site-name")}}</title>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-10 offset-md-1">
<h3>
Register for {{cfg("sr.ht", "site-name")}}
<small>
or <a href="/login">log in</a>
</small>
</h3>
</div>
</div>
{% if is_external_auth() %}
<p>Registration is disabled because {{cfg("sr.ht", "site-name")}} authentication
is managed by a different service. Please contact the system administrator
for further information.</p>
{% elif allow_registration() or invite_hash %}
{% if cfg("meta.sr.ht::billing", "enabled") == "yes" %}
<div class="row">
<div class="col-md-8 offset-md-2">
<p>
{% if payment %}
You are registering as a <strong>maintainer</strong>. After you complete
your registration, you will be taken to the billing page, where you'll
be provided information on payment options, financial aid, and so on.
<a href="{{url_for("auth.register")}}">Register as a contributor instead {{icon('caret-right')}}</a>
{% else %}
You are registering as a <strong>contributor</strong>, which is free but
will limit your access to some features. After you complete registration,
you can convert to a maintainer account by setting up billing on your
profile at any time.
<a href="{{url_for("auth.register")}}">Register as a maintainer instead {{icon('caret-right')}}</a>
{% endif %}
</p>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-6 offset-md-3">
<form method="POST" action="{{url_for("auth.register_step2_POST")}}">
{{csrf_token()}}
{% if invite_hash %}
<input type="hidden" name="invite_hash" value="{{invite_hash}}" />
<div class="alert alert-info">
You have received a special invitation to join {{cfg("sr.ht",
"site-name")}}. Sign up here!
</div>
{% endif %}
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
name="username"
id="username"
value="{{ username }}"
class="form-control {{valid.cls("username")}}"
required
autocomplete="username"
{% if not username %} autofocus{% endif %} />
{{valid.summary("username")}}
</div>
<div class="form-group">
<label for="email">Email address</label>
<input
type="email"
name="email"
id="email"
value="{{ email }}"
class="form-control {{valid.cls("email")}}"
required
{% if username and not email %} autofocus{% endif %} />
{{valid.summary("email")}}
{% if email and "+" in email %}
<input type="hidden" name="allow-plus-in-email" value="yes" />
<div class="alert alert-danger">
<strong>Warning</strong>: in order to use {{cfg("sr.ht",
"site-name")}} effectively, you must be able to both send <em>and</em>
receive emails from this email address. To continue, submit the form
again.
</div>
{% endif %}
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
name="password"
id="password"
value="{{ password }}"
class="form-control {{valid.cls("password")}}"
required
autocomplete="new-password"
{% if username and email and not password %} autofocus{% endif %} />
{{valid.summary("password")}}
</div>
{% if site_key %}
<div class="form-group">
<details>
<summary>PGP public key (optional)</summary>
<textarea
class="form-control {{valid.cls("pgp-key")}}"
id="pgp-key"
name="pgp-key"
style="font-family: monospace"
rows="5"
placeholder="gpg --armor --export-options export-minimal --export fingerprint…"
>{{pgp_key or ""}}</textarea>
<small class="form-text text-muted">
Emails sent from {{cfg("sr.ht", "site-name")}} are
signed with our PGP key:<br />
<a href="/privacy/pubkey">{{site_key}}</a>
<br />
If you add your PGP key here, we will also encrypt emails sent to
you. You may change this in your settings later, but if you enable
it now you must be able to decrypt the confirmation email to
complete registration.
</small>
{{valid.summary("pgp-key")}}
</details>
</div>
{% endif %}
<button class="btn btn-primary pull-right" type="submit">
Register {{icon("caret-right")}}
</button>
<p class="clearfix"></p>
</form>
</div>
</div>
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="alert alert-warning">
<strong>Privacy notice</strong>:
{{cfg("sr.ht", "site-name")}} collects the minimum amount of your personal
information which is necessary to faciliate the features of our services.
We do not collect or process any of your personal information for the
purposes of marketing or analytics. We do not send unsolicited marketing
emails. Your information is only shared with third-parties if it is
necessary to facilitate our services, and you will be warned before this
occurs and given an opportunity to prevent the transmission of your
information.
<a
href="{{cfg("sr.ht", "privacy-policy", default="https://man.sr.ht/privacy.md")}}"
rel="noopener"
target="_blank"
>Privacy policy {{icon('external-link-alt')}}</a>
</div>
</div>
</div>
{% else %}
<p>Registration is currently closed.</p>
{% endif %}
{% endblock %}

View File

@ -4,7 +4,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-10 offset-md-1">
<h3> <h3>
Register for {{cfg("sr.ht", "site-name")}} Register for {{cfg("sr.ht", "site-name")}}
<small> <small>
@ -18,116 +18,77 @@
is managed by a different service. Please contact the system administrator is managed by a different service. Please contact the system administrator
for further information.</p> for further information.</p>
{% elif allow_registration() or invite_hash %} {% elif allow_registration() or invite_hash %}
<div class="row"> <form
<div class="col-md-12"> class="row"
{% include "blurb.html" %} action="{{url_for("auth.register_POST")}}"
</div> method="POST"
</div> style="margin-bottom: 0" {# Look. I know. #}
<div class="row"> >
<div class="col-md-6 offset-md-3"> {{csrf_token()}}
<form method="POST" action="/register"> {% if invite_hash %}
{% if invite_hash %} <input type="hidden" name="invite_hash" value="{{invite_hash}}" />
<input type="hidden" name="invite_hash" value="{{invite_hash}}" /> {% endif %}
<div class="alert alert-info"> <div class="col-md-5 offset-md-1 event-list">
You have received a special invitation to join {{cfg("sr.ht", <div class="event">
"site-name")}}. Sign up here! <h3>Register as a contributor</h3>
</div> <p>
{% endif %} <strong>Signing up to contribute to a project here?</strong>
<div class="form-group"> <br />
<label for="username">Username</label> You may sign up to participate in projects hosted on
<input {{cfg("sr.ht", "site-name")}} for free. If you decide to host your own
type="text" projects here in the future, you can convert to a paid account later.
name="username" </p>
id="username" <button
value="{{ username }}" type="submit"
class="form-control {{valid.cls("username")}}" name="payment"
required value="no"
autocomplete="username" class="btn btn-primary btn-block"
{% if not username %} autofocus{% endif %} /> >
{{valid.summary("username")}} Sign up for free {{icon("caret-right")}}
</div>
<div class="form-group">
<label for="email">Email address</label>
<input
type="email"
name="email"
id="email"
value="{{ email }}"
class="form-control {{valid.cls("email")}}"
required
{% if username and not email %} autofocus{% endif %} />
{{valid.summary("email")}}
{% if email and "+" in email %}
<input type="hidden" name="allow-plus-in-email" value="yes" />
<div class="alert alert-danger">
<strong>Warning</strong>: in order to use {{cfg("sr.ht",
"site-name")}} effectively, you must be able to both send <em>and</em>
receive emails from this email address. To continue, submit the form
again.
</div>
{% endif %}
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
name="password"
id="password"
value="{{ password }}"
class="form-control {{valid.cls("password")}}"
required
autocomplete="new-password"
{% if username and email and not password %} autofocus{% endif %} />
{{valid.summary("password")}}
</div>
{% if site_key %}
<div class="form-group">
<details>
<summary><label for="pgp-key">PGP public key (optional)</label></summary>
<textarea
class="form-control {{valid.cls("pgp-key")}}"
id="pgp-key"
name="pgp-key"
style="font-family: monospace"
rows="5"
placeholder="gpg --armor --export-options export-minimal --export fingerprint…"
>{{pgp_key or ""}}</textarea>
<small class="form-text text-muted">
Emails sent from {{cfg("sr.ht", "site-name")}} are
signed with our PGP key:<br />
<a href="/privacy/pubkey">{{site_key}}</a>
<br />
If you add your PGP key here, we will also encrypt emails sent to
you. You may change this in your settings later, but if you enable
it now you must be able to decrypt the confirmation email to
complete registration.
</small>
{{valid.summary("pgp-key")}}
</details>
</div>
{% endif %}
<button class="btn btn-primary pull-right" type="submit">
Register {{icon("caret-right")}}
</button> </button>
<p class="clearfix"></p> </div>
</form>
</div> </div>
<div class="col-md-8 offset-md-2"> <div class="col-md-5">
<div class="alert alert-warning"> <div class="event">
<strong>Privacy notice</strong>: <h3>Register as a maintainer</h3>
{{cfg("sr.ht", "site-name")}} collects the minimum amount of your personal <p>
information which is necessary to faciliate the features of our services. <strong>Want to host your own projects here?</strong>
We do not collect or process any of your personal information for the <br />
purposes of marketing or analytics. For details, please review our Projects hosted on {{cfg("sr.ht", "site-name")}} are expected to pay for
<a their account. Financial aid is available for those in need. You can
href="{{cfg("sr.ht", "privacy-policy", default="https://man.sr.ht/privacy.md")}}" cancel at any time without losing access to your data.
rel="noopener" <a href="https://sourcehut.org/pricing" rel="noopener" target="_blank">
target="_blank" Pricing details {{icon('external-link-alt')}}
>privacy policy</a>. </a>
</p>
<button
type="submit"
name="payment"
value="yes"
class="btn btn-primary btn-block"
>
Sign up with payment {{icon("caret-right")}}
</button>
</div>
</div>
</form>
<div class="row">
<div class="col-md-10 offset-md-1">
<div class="alert alert-info">
Contributors can also skip registration entirely. You may submit or
comment on tickets, participate in discussions, and send patches to
projects on {{cfg("sr.ht", "site-name")}}, without signing up for an
account. You can find links to participate via email throughout the
logged-out version of many services.
</div> </div>
</div> </div>
</div> </div>
{% else %} {% else %}
<p>Registration is currently closed.</p> <div class="row">
<div class="col-md-10 offset-md-1">
<p>Registration is currently closed.</p>
</div>
</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -4,13 +4,13 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-8 offset-md-2">
<h3> <h3>
Check your email Registration successful
</h3> </h3>
<p> <p>
You will receive an email shortly with a link to complete registration. You will receive an email shortly with a link to complete your account
Contact registration. Contact support at
<a href="mailto:{{cfg("sr.ht", "owner-email")}}"> <a href="mailto:{{cfg("sr.ht", "owner-email")}}">
{{ "{} <{}>".format(cfg("sr.ht", "owner-name"), cfg("sr.ht", "owner-email")) }}</a> if you need help. {{ "{} <{}>".format(cfg("sr.ht", "owner-name"), cfg("sr.ht", "owner-email")) }}</a> if you need help.
</p> </p>

View File

@ -4,14 +4,14 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-12">
<h3> <h3>
Reset password Reset password
</h3> </h3>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6 offset-md-6">
<form method="POST"> <form method="POST">
{{csrf_token()}} {{csrf_token()}}
<div class="form-group"> <div class="form-group">

View File

@ -4,14 +4,14 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8 offset-md-2">
<h3> <h3>
TOTP Challenge TOTP Challenge
</h3> </h3>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8 offset-md-2">
<p> <p>
{% if challenge_type == "reset" %} {% if challenge_type == "reset" %}
This account has two-factor authentication enabled. You must complete a This account has two-factor authentication enabled. You must complete a