From cf5ad107ad84060efcfb512f40972ced6f4f3e54 Mon Sep 17 00:00:00 2001 From: gabixdev Date: Mon, 3 Dec 2018 21:06:47 +0100 Subject: [PATCH] new invite handling and unconfirmed account support --- litecord/blueprints/auth.py | 21 ++- litecord/blueprints/invites.py | 150 +++++++++--------- litecord/blueprints/users.py | 9 ++ litecord/schemas.py | 10 +- .../migration/scripts/9_nullable_emails.sql | 2 + schema.sql | 2 +- 6 files changed, 114 insertions(+), 80 deletions(-) create mode 100644 manage/cmd/migration/scripts/9_nullable_emails.sql diff --git a/litecord/blueprints/auth.py b/litecord/blueprints/auth.py index 9327f29..21f38ff 100644 --- a/litecord/blueprints/auth.py +++ b/litecord/blueprints/auth.py @@ -7,6 +7,7 @@ from quart import Blueprint, jsonify, request, current_app as app from litecord.auth import token_check, create_user from litecord.schemas import validate, REGISTER, REGISTER_WITH_INVITE from litecord.errors import BadRequest +from .invites import delete_invite, use_invite bp = Blueprint('auth', __name__) @@ -35,15 +36,29 @@ async def register(): """Register a single user.""" enabled = app.config.get('REGISTRATIONS') if not enabled: - return 'Registrations disabled', 405 + raise BadRequest('Registrations disabled', { + 'email': 'Registrations are disabled.' + }) - j = validate(await request.get_json(), REGISTER) - email, password, username = j['email'], j['password'], j['username'] + j = await request.get_json() + + if not 'password' in j: + j['password'] = 'default_password' # we need some password to make a token + + j = validate(j, REGISTER) + email, password, username, invite = j['email'] if 'email' in j else None, j['password'], j['username'], j['invite'] new_id, pwd_hash = await create_user( username, email, password, app.db ) + if j['invite']: + try: + await use_invite(new_id, j['invite']) + except Exception as e: + print(e) + pass # do nothing + return jsonify({ 'token': make_token(new_id, pwd_hash) }) diff --git a/litecord/blueprints/invites.py b/litecord/blueprints/invites.py index 06eee6f..51f92bb 100644 --- a/litecord/blueprints/invites.py +++ b/litecord/blueprints/invites.py @@ -19,6 +19,81 @@ from litecord.blueprints.checks import ( log = Logger(__name__) bp = Blueprint('invites', __name__) +# TODO: Ban handling +async def use_invite(user_id, invite_code): + """Try using an invite""" + inv = await app.db.fetchrow(""" + SELECT guild_id, created_at, max_age, uses, max_uses + FROM invites + WHERE code = $1 + """, invite_code) + + if inv is None: + raise BadRequest('Unknown invite') + + now = datetime.datetime.utcnow() + delta_sec = (now - inv['created_at']).total_seconds() + + if delta_sec > inv['max_age']: + await delete_invite(invite_code) + raise BadRequest('Unknown invite (expiried)') + + if inv['max_uses'] != -1 and inv['uses'] > inv['max_uses']: + await delete_invite(invite_code) + raise BadRequest('Unknown invite (too many uses)') + + guild_id = inv['guild_id'] + + joined = await app.db.fetchval(""" + SELECT joined_at + FROM members + WHERE user_id = $1 AND guild_id = $2 + """, user_id, guild_id) + + if joined is not None: + raise BadRequest('You are already in the guild') + + await app.db.execute(""" + INSERT INTO members (user_id, guild_id) + VALUES ($1, $2) + """, user_id, guild_id) + + await create_guild_settings(guild_id, user_id) + + # add the @everyone role to the invited member + await app.db.execute(""" + INSERT INTO member_roles (user_id, guild_id, role_id) + VALUES ($1, $2, $3) + """, user_id, guild_id, guild_id) + + await app.db.execute(""" + UPDATE invites + SET uses = uses + 1 + WHERE code = $1 + """, invite_code) + + # tell current members a new member came up + member = await app.storage.get_member_data_one(guild_id, user_id) + await app.dispatcher.dispatch_guild(guild_id, 'GUILD_MEMBER_ADD', { + **member, + **{ + 'guild_id': str(guild_id), + }, + }) + + # update member lists for the new member + await app.dispatcher.dispatch( + 'lazy_guild', guild_id, 'new_member', user_id) + + # subscribe new member to guild, so they get events n stuff + await app.dispatcher.sub('guild', guild_id, user_id) + + # tell the new member that theres the guild it just joined. + # we use dispatch_user_guild so that we send the GUILD_CREATE + # just to the shards that are actually tied to it. + guild = await app.storage.get_guild_full(guild_id, user_id, 250) + await app.dispatcher.dispatch_user_guild( + user_id, guild_id, 'GUILD_CREATE', guild) @bp.route('/channels//invites', methods=['POST']) async def create_invite(channel_id): @@ -148,82 +223,11 @@ async def get_channel_invites(channel_id: int): @bp.route('/invite/', methods=['POST']) -async def use_invite(invite_code): +async def _use_invite(invite_code): """Use an invite.""" user_id = await token_check() - inv = await app.db.fetchrow(""" - SELECT guild_id, created_at, max_age, uses, max_uses - FROM invites - WHERE code = $1 - """, invite_code) - - if inv is None: - raise BadRequest('Invite not found') - - now = datetime.datetime.utcnow() - delta_sec = (now - inv['created_at']).total_seconds() - - if delta_sec > inv['max_age']: - await delete_invite(invite_code) - raise BadRequest('Invite has expired (age).') - - if inv['uses'] > inv['max_uses']: - await delete_invite(invite_code) - raise BadRequest('Invite has expired (uses).') - - guild_id = inv['guild_id'] - - joined = await app.db.fetchval(""" - SELECT joined_at - FROM members - WHERE user_id = $1 AND guild_id = $2 - """, user_id, guild_id) - - if joined is not None: - raise BadRequest('You are already in the guild') - - await app.db.execute(""" - INSERT INTO members (user_id, guild_id) - VALUES ($1, $2) - """, user_id, guild_id) - - await create_guild_settings(guild_id, user_id) - - # add the @everyone role to the invited member - await app.db.execute(""" - INSERT INTO member_roles (user_id, guild_id, role_id) - VALUES ($1, $2, $3) - """, user_id, guild_id, guild_id) - - await app.db.execute(""" - UPDATE invites - SET uses = uses + 1 - WHERE code = $1 - """, invite_code) - - # tell current members a new member came up - member = await app.storage.get_member_data_one(guild_id, user_id) - await app.dispatcher.dispatch_guild(guild_id, 'GUILD_MEMBER_ADD', { - **member, - **{ - 'guild_id': str(guild_id), - }, - }) - - # update member lists for the new member - await app.dispatcher.dispatch( - 'lazy_guild', guild_id, 'new_member', user_id) - - # subscribe new member to guild, so they get events n stuff - await app.dispatcher.sub('guild', guild_id, user_id) - - # tell the new member that theres the guild it just joined. - # we use dispatch_user_guild so that we send the GUILD_CREATE - # just to the shards that are actually tied to it. - guild = await app.storage.get_guild_full(guild_id, user_id, 250) - await app.dispatcher.dispatch_user_guild( - user_id, guild_id, 'GUILD_CREATE', guild) + await use_invite(user_id, invite_code) # the reply is an invite object for some reason. inv = await app.storage.get_invite(invite_code) diff --git a/litecord/blueprints/users.py b/litecord/blueprints/users.py index 3354a46..79d8507 100644 --- a/litecord/blueprints/users.py +++ b/litecord/blueprints/users.py @@ -168,6 +168,10 @@ def to_update(j: dict, user: dict, field: str): async def _check_pass(j, user): + # Do not do password checks on unclaimed accounts + if user['email'] is None: + return + if not j['password']: raise BadRequest('password required', { 'password': 'password required' @@ -245,6 +249,11 @@ async def patch_me(): WHERE id = $2 """, new_icon.icon_hash, user_id) + if user['email'] is None and not 'new_password' in j: + raise BadRequest('missing password', { + 'password': 'Please set a password.' + }) + if 'new_password' in j and j['new_password']: await _check_pass(j, user) diff --git a/litecord/schemas.py b/litecord/schemas.py index 3037d52..42915c8 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -157,9 +157,13 @@ def validate(reqjson: Union[Dict, List], schema: Dict, REGISTER = { - 'email': {'type': 'email', 'required': True}, 'username': {'type': 'username', 'required': True}, - 'password': {'type': 'string', 'minlength': 5, 'required': True} + 'email': {'type': 'email', 'required': False}, + 'password': {'type': 'string', 'minlength': 5, 'required': False}, + 'invite': {'type': 'string', 'required': False, 'nullable': True}, # optional + 'fingerprint': {'type': 'string', 'required': False, 'nullable': True}, # these are sent by official client + 'captcha_key': {'type': 'string', 'required': False, 'nullable': True}, + 'consent': {'type': 'boolean'} } # only used by us, not discord, hence 'invcode' (to separate from discord) @@ -470,7 +474,7 @@ INVITE = { 'temporary': {'type': 'boolean', 'required': False, 'default': False}, 'unique': {'type': 'boolean', 'required': False, 'default': True}, - 'validate': {'type': 'boolean', 'required': False, 'nullable': True} + 'validate': {'type': 'string', 'required': False, 'nullable': True} # discord client sends invite code there } USER_SETTINGS = { diff --git a/manage/cmd/migration/scripts/9_nullable_emails.sql b/manage/cmd/migration/scripts/9_nullable_emails.sql new file mode 100644 index 0000000..85eb7c2 --- /dev/null +++ b/manage/cmd/migration/scripts/9_nullable_emails.sql @@ -0,0 +1,2 @@ +ALTER TABLE users ALTER COLUMN email DROP NOT NULL; +ALTER TABLE users ALTER COLUMN email SET DEFAULT NULL; \ No newline at end of file diff --git a/schema.sql b/schema.sql index bb35d8b..e3df14d 100644 --- a/schema.sql +++ b/schema.sql @@ -74,7 +74,7 @@ CREATE TABLE IF NOT EXISTS users ( id bigint UNIQUE NOT NULL, username text NOT NULL, discriminator varchar(4) NOT NULL, - email varchar(255) NOT NULL UNIQUE, + email varchar(255) DEFAULT NULL, -- user properties bot boolean DEFAULT FALSE,