M names/blueprints/contacts.py => names/blueprints/contacts.py +6 -5
@@ 10,17 10,18 @@ from uuid import uuid4
contacts = Blueprint("names.contacts", __name__)
+def format_number(n):
+ country, number = n.split(".")
+ n = phonenumbers.parse(f"+{country} {number}")
+ return phonenumbers.format_number(n,
+ phonenumbers.PhoneNumberFormat.INTERNATIONAL)
+
@contacts.route("/contacts")
@loginrequired
def contacts_GET():
contacts = (DomainContact.query
.filter(DomainContact.user_id == current_user.id))
contacts, pagination = paginate_query(contacts)
- def format_number(n):
- country, number = n.split(".")
- n = phonenumbers.parse(f"+{country} {number}")
- return phonenumbers.format_number(n,
- phonenumbers.PhoneNumberFormat.INTERNATIONAL)
return render_template("contacts.html",
contacts=contacts, iso3166=iso3166, format_number=format_number,
**pagination)
M names/blueprints/web.py => names/blueprints/web.py +95 -18
@@ 1,10 1,15 @@
import re
+import iso3166
from flask import Blueprint, render_template, request, redirect, url_for, abort
+from flask import session
from jinja2 import Markup
+from names.blueprints.contacts import format_number
+from names.decorators import paidloginrequired
from names.pygments import ZoneLexer
from names.types import User
from pygments import highlight
from pygments.formatters import HtmlFormatter
+from srht.config import get_origin
from srht.oauth import current_user, loginrequired, UserType
from srht.validation import Validation
@@ 27,7 32,6 @@ def index():
@web.route("/create")
@loginrequired
def create_GET():
- # TODO: Redirect to user contact info
return render_template("create.html")
@web.route("/create", methods=["POST"])
@@ 42,19 46,27 @@ def create_POST():
return render_template("create.html", **valid.kwargs), 400
return redirect(url_for(".create_domain_GET", domain=domain))
+def get_pricing_details(domain, years=1):
+ market_price = get_price(domain) * years
+ # Factor in our Stripe fees, so we don't lose money
+ fees = int((market_price * 0.029) + 30)
+ our_price = market_price + fees
+ return {
+ "market_price": market_price,
+ "our_price": our_price,
+ "fees": fees,
+ }
+
@web.route("/create/<domain>")
@loginrequired
def create_domain_GET(domain):
status = lookup_domain(domain)
- # TODO: Check if user has payment info on file
if status == DomainStatus.available:
- market_price = get_price(domain)
- # Factor in our Stripe fees, so we don't lose money
- our_price = int(market_price + (market_price * 0.029) + 30)
+ details = get_pricing_details(domain)
return render_template("create-domain.html",
DomainStatus=DomainStatus, status=status, domain=domain,
- our_price=our_price, market_price=market_price)
+ **details)
else:
transferable, transfer_email = check_transfer(domain)
# Note: this could be merged into lookup_domain and save us an API call
@@ 69,20 81,9 @@ def create_domain_GET(domain):
@loginrequired
def create_domain_POST(domain):
valid = Validation(request)
- permitted_users = [
- UserType.active_paying,
- UserType.active_free,
- UserType.admin,
- ]
if "purchase" in valid:
- if current_user.user_type not in permitted_users:
- return render_template("need-paid-account.html")
- # TODO: Check for contacts
- return render_template("purchase-domain.html")
+ return redirect(url_for(".purchase_domain_GET", domain=domain))
elif "transfer" in valid:
- if current_user.user_type not in permitted_users:
- return render_template("need-paid-account.html")
- # TODO: Check for contacts
return render_template("transfer-domain.html")
elif "configure" in valid:
# TODO: Configure domain
@@ 90,6 91,82 @@ def create_domain_POST(domain):
else:
abort(400)
+@web.route("/purchase/<domain>")
+@paidloginrequired
+def purchase_domain_GET(domain):
+ # TODO: Pull contacts out of the session if present
+ owner_contact = current_user.default_owner
+ admin_contact = current_user.default_admin
+ billing_contact = current_user.default_billing
+ tech_contact = current_user.default_tech
+ years = session.get("registration-years", default=1)
+ details = get_pricing_details(domain) # cache?
+ return render_template("purchase-domain.html",
+ owner_contact=owner_contact,
+ admin_contact=admin_contact,
+ billing_contact=billing_contact,
+ tech_contact=tech_contact,
+ iso3166=iso3166,
+ format_number=format_number,
+ years=years, domain=domain, **details)
+
+@web.route("/purchase/<domain>", methods=["POST"])
+@paidloginrequired
+def purchase_domain_POST(domain):
+ # TODO: Pull contacts out of the session if present
+ owner_contact = current_user.default_owner
+ admin_contact = current_user.default_admin
+ billing_contact = current_user.default_billing
+ tech_contact = current_user.default_tech
+
+ valid = Validation(request)
+ years = valid.require("years")
+ try:
+ years = int(years)
+ except:
+ valid.error("Expected registration period to be an integer",
+ field="years")
+ years = 1
+ # TODO: Validate years falls within permissible range for this TLD
+ valid.expect(1 <= years <= 10,
+ "Invalid registration period", field="years")
+ details = get_pricing_details(domain) # cache?
+
+ if not valid.ok:
+ return render_template("purchase-domain.html",
+ owner_contact=owner_contact,
+ admin_contact=admin_contact,
+ billing_contact=billing_contact,
+ tech_contact=tech_contact,
+ iso3166=iso3166,
+ format_number=format_number,
+ domain=domain, **details, **valid.kwargs)
+ session["registration-years"] = years
+ return redirect(url_for(".review_purchase_GET", domain=domain))
+
+@web.route("/purchase/review/<domain>")
+@paidloginrequired
+def review_purchase_GET(domain):
+ # TODO: Pull contacts out of the session if present
+ owner_contact = current_user.default_owner
+ admin_contact = current_user.default_admin
+ billing_contact = current_user.default_billing
+ tech_contact = current_user.default_tech
+ years = session.get("registration-years")
+ if not years:
+ return redirect(url_for(".purchase_domain_GET", domain=domain))
+ if not all([owner_contact, admin_contact, billing_contact, tech_contact]):
+ return redirect(url_for(".purchase_domain_GET", domain=domain))
+ details = get_pricing_details(domain, years) # cache?
+ return render_template("review-purchase.html",
+ owner_contact=owner_contact,
+ admin_contact=admin_contact,
+ billing_contact=billing_contact,
+ tech_contact=tech_contact,
+ iso3166=iso3166,
+ format_number=format_number,
+ years=years, domain=domain, **details)
+
@web.route("/mockup")
def mockup():
with open("/home/sircmpwn/sr.ht.zone") as f:
A names/decorators.py => names/decorators.py +26 -0
@@ 0,0 1,26 @@
+from flask import request, redirect, current_app
+from functools import wraps
+from srht.config import get_origin
+from srht.oauth import current_user, UserType
+from urllib.parse import quote_plus
+
+def paidloginrequired(f):
+ paid_users = [
+ UserType.active_paying,
+ UserType.active_free,
+ UserType.admin,
+ ]
+
+ @wraps(f)
+ def wrapper(*args, **kwargs):
+ billing_url = (get_origin("meta.sr.ht", external=True)
+ + "/billing/initial?return_to="
+ + quote_plus(request.url))
+ if not current_user:
+ return redirect(current_app.oauth_service.oauth_url(request.url))
+ elif current_user.user_type not in paid_users:
+ return render_template("need-paid-account.html",
+ billing_url=billing_url)
+ else:
+ return f(*args, **kwargs)
+ return wrapper
M names/templates/contacts.html => names/templates/contacts.html +18 -29
@@ 99,42 99,31 @@
<div class="col-md-4">
<h3>Default contacts</h3>
<div class="event-list">
+ {% macro display_contact(contact_type, contact) %}
<div class="event">
- <h4>Owner Contact</h4>
- John Doe — Acme, Inc<br />
- <a href="#">jdoe@example.org</a><br />
+ <h4>{{contact_type}}</h4>
+ {% if contact %}
+ {{contact.first_name}}
+ {{contact.last_name}}
+ —
+ {{contact.org_name}}<br />
+ <a href="mailto:{{contact.email}}">{{contact.email}}</a><br />
<span class="text-muted">
- b20968c6-8b99-42a0...
+ {{contact.uuid[:18]}}...
</span>
+ {# TODO: Contact review/edit page #}
<a href="#">{{icon("external-link-alt")}}</a>
- </div>
- <div class="event">
- <h4>Administrative Contact</h4>
- Jane Doe — Acme, Inc<br />
- <a href="#">jdoe@example.org</a><br />
- <span class="text-muted">
- b20968c6-8b99-42a0...
- </span>
- <a href="#">{{icon("external-link-alt")}}</a>
- </div>
- <div class="event">
- <h4>Billing Contact</h4>
- John Doe — Acme, Inc<br />
- <a href="#">jdoe@example.org</a><br />
+ {% else %}
<span class="text-muted">
- b20968c6-8b99-42a0...
+ No default specified.
</span>
- <a href="#">{{icon("external-link-alt")}}</a>
- </div>
- <div class="event">
- <h4>Technical Contact</h4>
- Jane Doe — Acme, Inc<br />
- <a href="#">jdoe@example.org</a><br />
- <span class="text-muted">
- b20968c6-8b99-42a0...
- </span>
- <a href="#">{{icon("external-link-alt")}}</a>
+ {% endif %}
</div>
+ {% endmacro %}
+ {{display_contact("Owner Contact", current_user.default_owner)}}
+ {{display_contact("Administrative Contact", current_user.default_admin)}}
+ {{display_contact("Billing Contact", current_user.default_billing)}}
+ {{display_contact("Technical Contact", current_user.default_tech)}}
</div>
<a
href="{{url_for("names.contacts.new_GET")}}"
M names/templates/create-domain.html => names/templates/create-domain.html +1 -1
@@ 26,7 26,7 @@
{{cfg("sr.ht", "site-name")}} account, we don't charge a markup.
</blockquote>
<button class="btn btn-primary" name="purchase">
- Purchase this domain {{icon("caret-right")}}
+ Configure purchase details {{icon("caret-right")}}
</button>
<button class="btn btn-default" name="configure">
Configure nameservers {{icon("caret-right")}}
M names/templates/need-paid-account.html => names/templates/need-paid-account.html +2 -4
@@ 8,8 8,7 @@
<p>
Domain registration is only available to paid accounts. This allows us to
avoid charging you a markup for our domains - you pay what we pay, plus
- our payment processor's fees. Please fill out your billing information on
- your profile, then refresh this page to continue.
+ our payment processor's fees.
</p>
{% if current_user.user_type.value == "active_delinquent" %}
<div class="alert alert-danger">
@@ 19,9 18,8 @@
</div>
{% endif %}
<a
- href="{{get_origin('meta.sr.ht', external=True)}}/billing/initial"
+ href="{{billing_url}}"
class="btn btn-primary"
- target="_blank"
>Continue to billing {{icon('caret-right')}}</a>
<a
href="{{url_for("names.web.index")}}"
A names/templates/purchase-domain.html => names/templates/purchase-domain.html +113 -0
@@ 0,0 1,113 @@
+{% extends "layout.html" %}
+{% block content %}
+<form method="POST">
+ <div class="row">
+ {{csrf_token()}}
+ <div class="col-md-12">
+ <h3>
+ Configure your purchase
+ </h3>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <p>The following domain contacts will be used for {{domain}}:</p>
+ <div class="event-list contact-list">
+ {% macro display_contact(contact_type, contact, is_default) %}
+ <div class="event">
+ <h4>
+ {{contact_type}}
+ </h4>
+ {% if contact %}
+ {{contact.first_name}}
+ {{contact.last_name}}
+ —
+ {{contact.org_name}}<br />
+ <a href="mailto:{{contact.email}}">{{contact.email}}</a><br />
+ {% if is_default %}
+ <div class="text-muted">(account default)</div>
+ {% else %}
+ <div>{{contact.uuid[:18]}}...</div>
+ {% endif %}
+ <a class="btn btn-default btn-block" href="#">
+ Use another contact {{icon('caret-right')}}
+ </a>
+ {% else %}
+ <span class="text-muted">
+ No default specified.
+ </span>
+ <a class="btn btn-primary" href="#">
+ Choose or create a contact {{icon('caret-right')}}
+ </a>
+ {% endif %}
+ </div>
+ {% endmacro %}
+ {{ display_contact("Owner", owner_contact,
+ owner_contact == current_user.default_owner) }}
+ {{ display_contact("Administrative", admin_contact,
+ admin_contact == current_user.default_admin) }}
+ {{ display_contact("Billing", billing_contact,
+ billing_contact == current_user.default_billing) }}
+ {{ display_contact("Technical", tech_contact,
+ tech_contact == current_user.default_tech) }}
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-8">
+ <div class="form-group">
+ <label for="years">Registration period (years)</label>
+ <input
+ class="form-control {{valid.cls('years')}}"
+ type="number"
+ value="{{years}}"
+ name="years"
+ id="years"
+ {# TODO: Set min/max based on TLD-specific parameters #}
+ min="1"
+ max="10"
+ required />
+ <small class="form-text text-muted">
+ <strong>{{domain}}</strong> is available for
+ ${{"{:.02f}".format(market_price / 100)}} per year plus
+ ${{"{:.02f}".format(fees / 100)}} in payment processing fees.
+ As you increase the registration period, you pay a smaller fee per
+ dollar spent (2.9% + 30¢).
+ </small>
+ {{valid.summary("years")}}
+ </div>
+ <div class="form-group">
+ <details>
+ <summary class="text-muted">
+ Additional registration options
+ </summary>
+ <label class="checkbox" for="privacy">
+ <input type="checkbox" name="privacy" id="privacy" checked />
+ Enable WHOIS Privacy for this domain
+ </label>
+ {# TODO: Add nameserver configuration #}
+ </details>
+ </div>
+ </div>
+ </div>
+ <div class="row" style="margin-top: 1rem">
+ <div class="col-md-2">
+ <a
+ href="{{url_for("names.web.create_domain_GET", domain=domain)}}"
+ class="btn btn-default btn-block"
+ >{{icon('caret-left')}} Back</a>
+ </div>
+ <div class="offset-md-3 col-md-3">
+ {% if owner_contact and admin_contact and billing_contact and tech_contact %}
+ <button class="btn btn-primary btn-block" name="purchase">
+ Review purchase {{icon('caret-right')}}
+ </button>
+ {% else %}
+ <button class="btn btn-primary btn-block" name="purchase" disabled>
+ Choose contacts to proceed
+ </button>
+ {% endif %}
+ </div>
+ </div>
+</form>
+{% endblock %}
D names/templates/register-domain.html => names/templates/register-domain.html +0 -19
@@ 1,19 0,0 @@
-{% extends "layout.html" %}
-{% block content %}
-<div class="row">
- <form class="col-md-12" method="POST">
- <h3>Register {{domain}}</h3>
- <p>
- <strong>{{domain}}</strong> is available for
- ${{"{:.02f}".format(price.cents / 100.0)}} per year!
- </p>
- {{csrf_token()}}
- <button class="btn btn-primary">
- Purchase this domain {{icon("caret-right")}}
- </button>
- <button class="btn btn-default">
- I just want DNS {{icon("caret-right")}}
- </button>
- </form>
-</div>
-{% endblock %}
A names/templates/review-purchase.html => names/templates/review-purchase.html +89 -0
@@ 0,0 1,89 @@
+{% extends "layout.html" %}
+{% block content %}
+<form method="POST">
+ <div class="row">
+ {{csrf_token()}}
+ <div class="col-md-12">
+ <h3>
+ Review your purchase
+ </h3>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-12">
+ <div class="event-list contact-list">
+ {% macro display_contact(contact_type, contact, is_default) %}
+ <div class="event">
+ <h4>
+ {{contact_type}}
+ </h4>
+ {{contact.first_name}}
+ {{contact.last_name}}
+ —
+ {{contact.org_name}}<br />
+ <a href="mailto:{{contact.email}}">{{contact.email}}</a><br />
+ </div>
+ {% endmacro %}
+ {{ display_contact("Owner", owner_contact,
+ owner_contact == current_user.default_owner) }}
+ {{ display_contact("Administrative", admin_contact,
+ admin_contact == current_user.default_admin) }}
+ {{ display_contact("Billing", billing_contact,
+ billing_contact == current_user.default_billing) }}
+ {{ display_contact("Technical", tech_contact,
+ tech_contact == current_user.default_tech) }}
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="offset-md-2 col-md-8">
+ <table class="table">
+ <thead>
+ <tr>
+ <th>Item</th>
+ <th>Price</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ {% if years == 1 %}
+ <td>{{domain}} registration ({{years}} year)</td>
+ {% else %}
+ <td>{{domain}} registration ({{years}} years)</td>
+ {% endif %}
+ <td
+ style="text-align: right"
+ >${{"{:.02f}".format(market_price / 100.0)}}</td>
+ </tr>
+ <tr>
+ <td>Processing fees</td>
+ <td
+ style="text-align: right"
+ >${{"{:.02f}".format(fees / 100.0)}}</td>
+ </tr>
+ <tr>
+ <td style="border: none; text-align: right">Total</td>
+ <td
+ style="text-align: right; font-weight: bold"
+ >${{"{:.02f}".format(our_price / 100.0)}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <div class="row">
+ <div class="offset-md-2 col-md-2">
+ <a
+ href="{{url_for("names.web.purchase_domain_GET", domain=domain)}}"
+ class="btn btn-default btn-block"
+ >{{icon('caret-left')}} Make changes</a>
+ </div>
+ <div class="offset-md-3 col-md-3">
+ <button class="btn btn-primary btn-block">
+ Complete purchase {{icon('caret-right')}}
+ </button>
+ </div>
+ </div>
+</form>
+{% endblock %}
+
M scss/main.scss => scss/main.scss +22 -0
@@ 31,3 31,25 @@ h4 small {
}
}
}
+
+.contact-list {
+ display: flex;
+ margin: 0 -15px 1rem;
+
+ .event {
+ width: calc(25% - 30px);
+ margin: 0 15px;
+ }
+
+ .btn-block {
+ margin-top: 0.5rem;
+ }
+}
+
+.registration-term {
+ display: none;
+}
+
+#years[value="1"] ~ .registration-term.term-1-years {
+ display: table;
+}