all: drop support for user invites

This commit is contained in:
Drew DeVault 2023-02-13 10:16:33 +01:00
parent 226c332127
commit 0bb0b3e688
18 changed files with 51 additions and 249 deletions

View File

@ -476,8 +476,7 @@ type Mutation {
registerAccount(email: String!,
username: String!,
password: String!,
pgpKey: String,
invite: String): User @anoninternal
pgpKey: String): User @anoninternal
"""
Registers an OAuth client. Only OAuth 2.0 confidental clients are

View File

@ -17,7 +17,6 @@ import (
"log"
"net/url"
"sort"
"strconv"
"strings"
"time"
@ -614,7 +613,7 @@ func (r *mutationResolver) DeleteWebhook(ctx context.Context, id int) (model.Web
}
// RegisterAccount is the resolver for the registerAccount field.
func (r *mutationResolver) RegisterAccount(ctx context.Context, email string, username string, password string, pgpKey *string, invite *string) (*model.User, error) {
func (r *mutationResolver) RegisterAccount(ctx context.Context, email string, username string, password string, pgpKey *string) (*model.User, error) {
// Note: this resolver is used with anonymous internal auth, so most of the
// fields in auth.ForContext(ctx) are invalid.
valid := valid.New(ctx)
@ -688,16 +687,6 @@ func (r *mutationResolver) RegisterAccount(ctx context.Context, email string, us
return nil, nil
}
invites := 0
inv, ok := conf.Get("meta.sr.ht::settings", "user-invites")
if ok {
var err error
invites, err = strconv.Atoi(inv)
if err != nil {
panic(err)
}
}
pwhash, err := bcrypt.GenerateFromPassword(
[]byte(password), bcrypt.DefaultCost)
if err != nil {
@ -729,14 +718,14 @@ func (r *mutationResolver) RegisterAccount(ctx context.Context, email string, us
row = tx.QueryRowContext(ctx, `
INSERT INTO "user" (
created, updated, username, email, user_type, password,
confirmation_hash, invites
confirmation_hash
) VALUES (
NOW() at time zone 'utc',
NOW() at time zone 'utc',
$1, $2, 'unconfirmed', $3, $4, $5
$1, $2, 'unconfirmed', $3, $4
)
RETURNING id, created, updated, username, email, user_type;
`, username, email, string(pwhash), confirmation, invites)
`, username, email, string(pwhash), confirmation)
if err := row.Scan(&user.ID, &user.Created, &user.Updated,
&user.Username, &user.Email, &user.UserTypeRaw); err != nil {
@ -757,25 +746,6 @@ func (r *mutationResolver) RegisterAccount(ctx context.Context, email string, us
return err
}
if invite != nil {
row = tx.QueryRowContext(ctx, `
UPDATE invite
SET recipient_id = $1
WHERE invite_hash = $2 AND recipient_id IS NULL
RETURNING id;
`, user.ID, *invite)
var id int
if err := row.Scan(&id); err != nil {
if err == sql.ErrNoRows {
valid.Error("The invite code you've used is invalid or expired.").
WithField("invite")
return errors.New("placeholder")
}
return err
}
}
addr := server.RemoteAddr(ctx)
_, err = tx.ExecContext(ctx, `
INSERT INTO audit_log_entry (

View File

@ -157,10 +157,6 @@ registration=no
#
# Where to redirect new users upon registration
onboarding-redirect=http://example.org
#
# How many invites each user is issued upon registration (only applicable if
# open registration is disabled)
user-invites=5
[meta.sr.ht::aliases]
#

View File

@ -130,7 +130,6 @@ if __name__ == '__main__':
else:
validate_user(username, email)
user = User(username)
user.invites = cfg("meta.sr.ht::settings", "user-invites", default=0)
db.session.add(user)
if set_password:

View File

@ -0,0 +1,36 @@
"""Drop invites
Revision ID: 2c272378490d
Revises: 20ca7d8cb982
Create Date: 2023-02-13 10:09:52.930567
"""
# revision identifiers, used by Alembic.
revision = '2c272378490d'
down_revision = '20ca7d8cb982'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.execute("""
ALTER TABLE "user" DROP COLUMN invites;
DROP TABLE invite;
""")
def downgrade():
op.execute("""
ALTER TABLE "user" ADD COLUMN invites integer DEFAULT 0;
CREATE TABLE invite (
id serial PRIMARY KEY,
created timestamp without time zone NOT NULL,
updated timestamp without time zone NOT NULL,
invite_hash character varying(128),
sender_id integer,
recipient_id integer
);
""")

View File

@ -17,7 +17,6 @@ class MetaApp(SrhtFlask):
from metasrht.blueprints.api import register_api
from metasrht.blueprints.auth import auth
from metasrht.blueprints.invites import invites
from metasrht.blueprints.keys import keys
from metasrht.blueprints.oauth_exchange import oauth_exchange
from metasrht.blueprints.oauth_web import oauth_web
@ -29,7 +28,6 @@ class MetaApp(SrhtFlask):
from srht.graphql import gql_blueprint
self.register_blueprint(auth)
self.register_blueprint(invites)
self.register_blueprint(keys)
self.register_blueprint(oauth_exchange)
self.register_blueprint(oauth_web)

View File

@ -104,7 +104,6 @@ class PamAuthMethod(AuthMethod):
user = User(username)
user.email = f'{username}@{self.domain}'
user.password = ''
user.invites = cfg("meta.sr.ht::settings", "user-invites", default=0)
user.confirmation_hash = None
user.user_type = UserType.active_non_paying

View File

@ -10,7 +10,7 @@ from metasrht.auth_validation import validate_password
from metasrht.blueprints.security import metrics as security_metrics
from metasrht.email import send_email_notification
from metasrht.totp import totp
from metasrht.types import User, UserType, Invite
from metasrht.types import User, UserType
from metasrht.types import UserAuthFactor, FactorType, PGPKey
from metasrht.webhooks import UserWebhook
from prometheus_client import Counter
@ -117,62 +117,44 @@ def register():
return redirect(url_for("auth.register_step2_GET"))
return render_template("register.html", site_key=site_key_id)
@auth.route("/register/<invite>")
def register_invite(invite):
if current_user:
return redirect("/")
if is_external_auth():
return render_template("register.html")
return render_template("register.html", site_key=site_key_id, invite=invite)
@auth.route("/register", methods=["POST"])
def register_POST():
is_open = allow_registration()
valid = Validation(request)
payment = valid.require("payment")
invite = valid.optional("invite")
if not valid.ok:
abort(400)
payment = payment == "yes"
if invite:
session["invite"] = invite
session["payment"] = payment
return redirect(url_for("auth.register_step2_GET"))
@auth.route("/register/step2")
def register_step2_GET():
invite = session.get("invite")
payment = session.get("payment", "no")
if current_user:
return redirect("/")
return render_template("register-step2.html",
site_key=site_key_id, invite=invite, payment=payment)
site_key=site_key_id, payment=payment)
@auth.route("/register/step2", methods=["POST"])
def register_step2_POST():
if current_user:
abort(400)
is_open = allow_registration()
session.pop("invite", None)
payment = session.get("payment", False)
valid = Validation(request)
username = valid.require("username", friendly_name="Username")
email = valid.require("email", friendly_name="Email address")
password = valid.require("password", friendly_name="Password")
invite = valid.optional("invite", default=None)
pgpKey = valid.optional("pgpKey", default=None)
if not invite:
invite = None
if not pgpKey:
pgpKey = None
if not valid.ok:
return render_template("register-step2.html",
is_open=(is_open or invite is not None),
return render_template("register-step2.html", is_open=is_open,
site_key=site_key_id, payment=payment, **valid.kwargs), 400
if is_abuse(valid):
@ -180,23 +162,21 @@ def register_step2_POST():
allow_plus_in_email = valid.optional("allow-plus-in-email")
if "+" in email and allow_plus_in_email != "yes":
return render_template("register-step2.html",
is_open=(is_open or invite is not None),
return render_template("register-step2.html", is_open=is_open,
site_key=site_key_id, payment=payment, **valid.kwargs), 400
resp = exec_gql("meta.sr.ht", """
mutation RegisterAccount($email: String!, $username: String!,
$password: String!, $pgpKey: String, $invite: String) {
$password: String!, $pgpKey: String) {
registerAccount(email: $email, username: $username,
password: $password, pgpKey: $pgpKey, invite: $invite) {
password: $password, pgpKey: $pgpKey) {
id
}
}
""", valid=valid, user=internal_anon, username=username,
email=email, password=password, pgpKey=pgpKey, invite=invite)
email=email, password=password, pgpKey=pgpKey)
if not valid.ok:
return render_template("register-step2.html",
is_open=(is_open or invite is not None),
return render_template("register-step2.html", is_open=is_open,
site_key=site_key_id, payment=payment, **valid.kwargs), 400
metrics.meta_registrations.inc()

View File

@ -1,40 +0,0 @@
from flask import Blueprint, render_template, redirect, abort
from metasrht.types import Invite, UserType
from srht.config import cfg
from srht.database import db
from srht.flask import session
from srht.oauth import current_user, loginrequired
invites = Blueprint('invites', __name__)
site_name = cfg("sr.ht", "site-name")
site_root = cfg("meta.sr.ht", "origin")
@invites.route("/invites")
@loginrequired
def index():
return render_template("invite.html")
@invites.route("/invites/gen-invite", methods=["POST"])
@loginrequired
def gen_invite():
if current_user.invites == 0 and current_user.user_type != UserType.admin:
abort(401)
invite = Invite()
invite.sender_id = current_user.id
if current_user.invites > 0:
current_user.invites -= 1
db.session.add(invite)
db.session.commit()
session["invite_link"] = "{}/register/{}".format(
site_root, invite.invite_hash)
return redirect("/invites/generated")
@invites.route("/invites/generated")
@loginrequired
def view_invite():
invite_link = session.get("invite_link")
if not invite_link:
return redirect("/invites")
del session["invite_link"]
return render_template("invite-link-generated.html", link=invite_link)

View File

@ -1,22 +0,0 @@
{% extends "meta.html" %}
{% block title %}
<title>Invite generated - {{cfg("sr.ht", "site-name")}} meta</title>
{% endblock %}
{% block content %}
<div class="row">
<section class="col-md-6">
<p>
This is a one-time use link that can be used to create an account on this
site.
</p>
<blockquote class="text-centered">
<strong>
<a href="{{ link }}">{{ link }}</a>
</strong>
</blockquote>
<a href="/invites" class="btn btn-default">
Continue {{icon("caret-right")}}
</a>
</section>
</div>
{% endblock %}

View File

@ -1,57 +0,0 @@
{% extends "meta.html" %}
{% block title %}
<title>Invitations - {{cfg("sr.ht", "site-name")}} meta</title>
{% endblock %}
{% block content %}
<div class="row">
<section class="col-md-7">
<p>
You have {{ current_user.invites }}
invite{{ 's' if current_user.invites != 1 else '' }} remaining with which
to invite people to {{ cfg("sr.ht", "site-name") }}. If you need more,
reach out to <a
href="mailto:{{cfg("sr.ht", "owner-email")}}"
>
{{cfg("sr.ht", "owner-name")}}
&lt;{{cfg("sr.ht", "owner-email")}}&gt;
</a>.
</p>
{% if current_user.user_type == UserType.admin %}
<div class="alert alert-info">
Admins have unlimited invites.
</div>
{% else %}
<div class="alert alert-warning">
You may be held accountable for the actions of users you invite. Please
exercise due care when giving them out.
</div>
{% endif %}
{% if current_user.invites != 0 or current_user.user_type == UserType.admin %}
<form method="POST" action="/invites/gen-invite">
{{csrf_token()}}
<button class="btn btn-primary" type="submit">
Generate link {{icon("caret-right")}}
</button>
</form>
{% endif %}
{% if current_user.invites_sent %}
<h3>Invites sent</h3>
<ul>
{% for invite in current_user.invites_sent %}
<li>
{% if invite.recipient_id %}
<code>{{invite.invite_hash}}</code>
(claimed by ~{{ invite.recipient.username}})
{% else %}
<code>
<a href="/register/{{invite.invite_hash}}">{{invite.invite_hash}}</a>
</code>
(unclaimed)
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</section>
</div>
{% endblock %}

View File

@ -17,7 +17,7 @@
<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 %}
{% elif allow_registration() %}
{% if cfg("meta.sr.ht::billing", "enabled") == "yes" %}
<div class="row">
<div class="col-md-10 offset-md-1">
@ -42,13 +42,6 @@
<div class="col-md-6 offset-md-3">
<form method="POST" action="{{url_for("auth.register_step2_POST")}}">
{{csrf_token()}}
{% if invite %}
<input type="hidden" name="invite" value="{{invite}}" />
<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

View File

@ -17,7 +17,7 @@
<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 %}
{% elif allow_registration() %}
<form
class="row"
action="{{url_for("auth.register_POST")}}"
@ -25,9 +25,6 @@
style="margin-bottom: 0" {# Look. I know. #}
>
{{csrf_token()}}
{% if invite %}
<input type="hidden" name="invite" value="{{invite}}" />
{% endif %}
<div class="col-md-5 offset-md-1 event-list">
<div class="event">
<h3>Register as a contributor</h3>

View File

@ -28,11 +28,6 @@
{% endif %}
</li>
{% endif %}
{% if not allow_registration() %}
<li class="nav-item invite-tab">
{{ link("/invites", "invites ({})".format(current_user.invites)) }}
</li>
{% endif %}
{% if current_user.user_type == UserType.admin %}
<li class="nav-item">
{{link("/users", "user admin", cls="text-danger")}}

View File

@ -7,7 +7,6 @@ from .oauthclient import *
from .oauthtoken import *
from .delegatedscope import *
from .revocationurl import *
from .invite import *
from .invoice import *
from .oauth2client import *
from .oauth2grant import *

View File

@ -1,28 +0,0 @@
import sqlalchemy as sa
import sqlalchemy_utils as sau
from srht.database import Base
import base64
import os
class Invite(Base):
__tablename__ = 'invite'
id = sa.Column(sa.Integer, primary_key=True)
created = sa.Column(sa.DateTime, nullable=False)
updated = sa.Column(sa.DateTime, nullable=False)
invite_hash = sa.Column(sa.String(128))
sender_id = sa.Column(sa.Integer, sa.ForeignKey('user.id'))
sender = sa.orm.relationship('User',
backref=sa.orm.backref('invites_sent'),
foreign_keys=[sender_id])
recipient_id = sa.Column(sa.Integer, sa.ForeignKey('user.id'))
recipient = sa.orm.relationship('User', foreign_keys=[recipient_id])
def gen_invite_hash(self):
self.invite_hash = base64.urlsafe_b64encode(os.urandom(18)) \
.decode('utf-8')
def __init__(self):
self.gen_invite_hash()
def __repr__(self):
return '<Invite {}>'.format(self.id)

View File

@ -30,8 +30,6 @@ class User(Base, UserMixin):
pgp_key = sa.orm.relationship('PGPKey', foreign_keys=[pgp_key_id])
reset_hash = sa.Column(sa.String(128))
reset_expiry = sa.Column(sa.DateTime())
invites = sa.Column(sa.Integer, server_default='0')
"Number of invites this user can send"
stripe_customer = sa.Column(sa.String(256))
payment_cents = sa.Column(
sa.Integer, nullable=False, server_default='0')

View File

@ -30,7 +30,6 @@ CREATE TABLE "user" (
pgp_key_id integer,
reset_hash character varying(128),
reset_expiry timestamp without time zone,
invites integer DEFAULT 0,
stripe_customer character varying(256),
payment_cents integer DEFAULT 0 NOT NULL,
payment_interval character varying DEFAULT 'monthly'::character varying,
@ -49,15 +48,6 @@ CREATE TABLE audit_log_entry (
details character varying(512)
);
CREATE TABLE invite (
id serial PRIMARY KEY,
created timestamp without time zone NOT NULL,
updated timestamp without time zone NOT NULL,
invite_hash character varying(128),
sender_id integer,
recipient_id integer
);
CREATE TABLE invoice (
id serial PRIMARY KEY,
created timestamp without time zone NOT NULL,