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!, registerAccount(email: String!,
username: String!, username: String!,
password: String!, password: String!,
pgpKey: String, pgpKey: String): User @anoninternal
invite: String): User @anoninternal
""" """
Registers an OAuth client. Only OAuth 2.0 confidental clients are Registers an OAuth client. Only OAuth 2.0 confidental clients are

View File

@ -17,7 +17,6 @@ import (
"log" "log"
"net/url" "net/url"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
@ -614,7 +613,7 @@ func (r *mutationResolver) DeleteWebhook(ctx context.Context, id int) (model.Web
} }
// RegisterAccount is the resolver for the registerAccount field. // 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 // Note: this resolver is used with anonymous internal auth, so most of the
// fields in auth.ForContext(ctx) are invalid. // fields in auth.ForContext(ctx) are invalid.
valid := valid.New(ctx) valid := valid.New(ctx)
@ -688,16 +687,6 @@ func (r *mutationResolver) RegisterAccount(ctx context.Context, email string, us
return nil, nil 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( pwhash, err := bcrypt.GenerateFromPassword(
[]byte(password), bcrypt.DefaultCost) []byte(password), bcrypt.DefaultCost)
if err != nil { if err != nil {
@ -729,14 +718,14 @@ func (r *mutationResolver) RegisterAccount(ctx context.Context, email string, us
row = tx.QueryRowContext(ctx, ` row = tx.QueryRowContext(ctx, `
INSERT INTO "user" ( INSERT INTO "user" (
created, updated, username, email, user_type, password, created, updated, username, email, user_type, password,
confirmation_hash, invites confirmation_hash
) VALUES ( ) VALUES (
NOW() at time zone 'utc', NOW() at time zone 'utc',
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; 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, if err := row.Scan(&user.ID, &user.Created, &user.Updated,
&user.Username, &user.Email, &user.UserTypeRaw); err != nil { &user.Username, &user.Email, &user.UserTypeRaw); err != nil {
@ -757,25 +746,6 @@ func (r *mutationResolver) RegisterAccount(ctx context.Context, email string, us
return err 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) addr := server.RemoteAddr(ctx)
_, err = tx.ExecContext(ctx, ` _, err = tx.ExecContext(ctx, `
INSERT INTO audit_log_entry ( INSERT INTO audit_log_entry (

View File

@ -157,10 +157,6 @@ registration=no
# #
# Where to redirect new users upon registration # Where to redirect new users upon registration
onboarding-redirect=http://example.org 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] [meta.sr.ht::aliases]
# #

View File

@ -130,7 +130,6 @@ if __name__ == '__main__':
else: else:
validate_user(username, email) validate_user(username, email)
user = User(username) user = User(username)
user.invites = cfg("meta.sr.ht::settings", "user-invites", default=0)
db.session.add(user) db.session.add(user)
if set_password: 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.api import register_api
from metasrht.blueprints.auth import auth from metasrht.blueprints.auth import auth
from metasrht.blueprints.invites import invites
from metasrht.blueprints.keys import keys from metasrht.blueprints.keys import keys
from metasrht.blueprints.oauth_exchange import oauth_exchange from metasrht.blueprints.oauth_exchange import oauth_exchange
from metasrht.blueprints.oauth_web import oauth_web from metasrht.blueprints.oauth_web import oauth_web
@ -29,7 +28,6 @@ class MetaApp(SrhtFlask):
from srht.graphql import gql_blueprint from srht.graphql import gql_blueprint
self.register_blueprint(auth) self.register_blueprint(auth)
self.register_blueprint(invites)
self.register_blueprint(keys) self.register_blueprint(keys)
self.register_blueprint(oauth_exchange) self.register_blueprint(oauth_exchange)
self.register_blueprint(oauth_web) self.register_blueprint(oauth_web)

View File

@ -104,7 +104,6 @@ class PamAuthMethod(AuthMethod):
user = User(username) user = User(username)
user.email = f'{username}@{self.domain}' user.email = f'{username}@{self.domain}'
user.password = '' user.password = ''
user.invites = cfg("meta.sr.ht::settings", "user-invites", default=0)
user.confirmation_hash = None user.confirmation_hash = None
user.user_type = UserType.active_non_paying 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.blueprints.security import metrics as security_metrics
from metasrht.email import send_email_notification from metasrht.email import send_email_notification
from metasrht.totp import totp 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.types import UserAuthFactor, FactorType, PGPKey
from metasrht.webhooks import UserWebhook from metasrht.webhooks import UserWebhook
from prometheus_client import Counter from prometheus_client import Counter
@ -117,62 +117,44 @@ def register():
return redirect(url_for("auth.register_step2_GET")) 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>")
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"]) @auth.route("/register", methods=["POST"])
def register_POST(): def register_POST():
is_open = allow_registration() is_open = allow_registration()
valid = Validation(request) valid = Validation(request)
payment = valid.require("payment") payment = valid.require("payment")
invite = valid.optional("invite")
if not valid.ok: if not valid.ok:
abort(400) abort(400)
payment = payment == "yes" payment = payment == "yes"
if invite:
session["invite"] = invite
session["payment"] = payment session["payment"] = payment
return redirect(url_for("auth.register_step2_GET")) return redirect(url_for("auth.register_step2_GET"))
@auth.route("/register/step2") @auth.route("/register/step2")
def register_step2_GET(): def register_step2_GET():
invite = session.get("invite")
payment = session.get("payment", "no") payment = session.get("payment", "no")
if current_user: if current_user:
return redirect("/") return redirect("/")
return render_template("register-step2.html", 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"]) @auth.route("/register/step2", methods=["POST"])
def register_step2_POST(): def register_step2_POST():
if current_user: if current_user:
abort(400) abort(400)
is_open = allow_registration() is_open = allow_registration()
session.pop("invite", None)
payment = session.get("payment", False) payment = session.get("payment", False)
valid = Validation(request) 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")
invite = valid.optional("invite", default=None)
pgpKey = valid.optional("pgpKey", default=None) pgpKey = valid.optional("pgpKey", default=None)
if not invite:
invite = None
if not pgpKey: if not pgpKey:
pgpKey = None pgpKey = None
if not valid.ok: if not valid.ok:
return render_template("register-step2.html", return render_template("register-step2.html", is_open=is_open,
is_open=(is_open or invite is not None),
site_key=site_key_id, payment=payment, **valid.kwargs), 400 site_key=site_key_id, payment=payment, **valid.kwargs), 400
if is_abuse(valid): if is_abuse(valid):
@ -180,23 +162,21 @@ def register_step2_POST():
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-step2.html", return render_template("register-step2.html", is_open=is_open,
is_open=(is_open or invite is not None),
site_key=site_key_id, payment=payment, **valid.kwargs), 400 site_key=site_key_id, payment=payment, **valid.kwargs), 400
resp = exec_gql("meta.sr.ht", """ resp = exec_gql("meta.sr.ht", """
mutation RegisterAccount($email: String!, $username: String!, mutation RegisterAccount($email: String!, $username: String!,
$password: String!, $pgpKey: String, $invite: String) { $password: String!, $pgpKey: String) {
registerAccount(email: $email, username: $username, registerAccount(email: $email, username: $username,
password: $password, pgpKey: $pgpKey, invite: $invite) { password: $password, pgpKey: $pgpKey) {
id id
} }
} }
""", valid=valid, user=internal_anon, username=username, """, 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: if not valid.ok:
return render_template("register-step2.html", return render_template("register-step2.html", is_open=is_open,
is_open=(is_open or invite is not None),
site_key=site_key_id, payment=payment, **valid.kwargs), 400 site_key=site_key_id, payment=payment, **valid.kwargs), 400
metrics.meta_registrations.inc() 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 <p>Registration is disabled because {{cfg("sr.ht", "site-name")}} authentication
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 %} {% elif allow_registration() %}
{% if cfg("meta.sr.ht::billing", "enabled") == "yes" %} {% if cfg("meta.sr.ht::billing", "enabled") == "yes" %}
<div class="row"> <div class="row">
<div class="col-md-10 offset-md-1"> <div class="col-md-10 offset-md-1">
@ -42,13 +42,6 @@
<div class="col-md-6 offset-md-3"> <div class="col-md-6 offset-md-3">
<form method="POST" action="{{url_for("auth.register_step2_POST")}}"> <form method="POST" action="{{url_for("auth.register_step2_POST")}}">
{{csrf_token()}} {{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"> <div class="form-group">
<label for="username">Username</label> <label for="username">Username</label>
<input <input

View File

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

View File

@ -28,11 +28,6 @@
{% endif %} {% endif %}
</li> </li>
{% endif %} {% 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 %} {% if current_user.user_type == UserType.admin %}
<li class="nav-item"> <li class="nav-item">
{{link("/users", "user admin", cls="text-danger")}} {{link("/users", "user admin", cls="text-danger")}}

View File

@ -7,7 +7,6 @@ from .oauthclient import *
from .oauthtoken import * from .oauthtoken import *
from .delegatedscope import * from .delegatedscope import *
from .revocationurl import * from .revocationurl import *
from .invite import *
from .invoice import * from .invoice import *
from .oauth2client import * from .oauth2client import *
from .oauth2grant 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]) pgp_key = sa.orm.relationship('PGPKey', foreign_keys=[pgp_key_id])
reset_hash = sa.Column(sa.String(128)) reset_hash = sa.Column(sa.String(128))
reset_expiry = sa.Column(sa.DateTime()) 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)) stripe_customer = sa.Column(sa.String(256))
payment_cents = sa.Column( payment_cents = sa.Column(
sa.Integer, nullable=False, server_default='0') sa.Integer, nullable=False, server_default='0')

View File

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