all: drop support for user invites
This commit is contained in:
parent
226c332127
commit
0bb0b3e688
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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]
|
||||
#
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
);
|
||||
""")
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
|
@ -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 %}
|
|
@ -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")}}
|
||||
<{{cfg("sr.ht", "owner-email")}}>
|
||||
</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 %}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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")}}
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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)
|
|
@ -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')
|
||||
|
|
10
schema.sql
10
schema.sql
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue