mirror of https://gitlab.com/litecord/litecord.git
litecord.blueprints: add auth blueprint
- Pipfile: add bcrypt and itsdangerous - litecord: add errors module - litecord: add snowflake module - run: add error handlers - schema: add UNIQUE constraint to users.email - remove users.password_salt
This commit is contained in:
parent
6b066bba05
commit
f7f387dcf0
2
Pipfile
2
Pipfile
|
|
@ -4,6 +4,8 @@ verify_ssl = true
|
|||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
bcrypt = "==3.1.4"
|
||||
itsdangerous = "==0.24"
|
||||
asyncpg = "==0.16.0"
|
||||
Quart = "==0.6.0"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "027981db9b70eaab2e7c72ffc69501db32404f00676eb0229c2b0370d3b50ed8"
|
||||
"sha256": "e8807281b43032a3fde4598f48c98436b8ef50fe332a413e71c37062a16b61d9"
|
||||
},
|
||||
"host-environment-markers": {
|
||||
"implementation_name": "cpython",
|
||||
"implementation_version": "3.6.5",
|
||||
"os_name": "posix",
|
||||
"platform_machine": "x86_64",
|
||||
"platform_python_implementation": "CPython",
|
||||
"platform_release": "4.16.13-2-ARCH",
|
||||
"platform_system": "Linux",
|
||||
"platform_version": "#1 SMP PREEMPT Fri Jun 1 18:46:11 UTC 2018",
|
||||
"python_full_version": "3.6.5",
|
||||
"python_version": "3.6",
|
||||
"sys_platform": "linux"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
|
@ -17,142 +30,91 @@
|
|||
},
|
||||
"default": {
|
||||
"aiofiles": {
|
||||
"hashes": [
|
||||
"sha256:25c66ea3872d05d53292a6b3f7fa0f86691512076446d83a505d227b5e76f668",
|
||||
"sha256:852a493a877b73e11823bfd4e8e5ef2610d70d12c9eaed961bcd9124d8de8c10"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==0.3.2"
|
||||
},
|
||||
"asyncpg": {
|
||||
"hashes": [
|
||||
"sha256:166c8e094de78ccbfc598a5342037a6ca5d7ee1e8144b3cfade244dd591b1ed0",
|
||||
"sha256:2913b7cffdfb5bf1da5ed751485b559d1f1990be005d6d63d3ca0bf09a9d8ee6",
|
||||
"sha256:31d5a9d993ce97924d9601bf6a37bb8b542d63bc8716b36238511e5e5915b14c",
|
||||
"sha256:440dc17ec98c2e69f58947a591eed5967724794c876b1d6e53950e9b0b561788",
|
||||
"sha256:5791554375c71ef339ee01fafb931f593c9f3ec85a5db9753c185199cee6c87e",
|
||||
"sha256:600e6e14078be26e2322dfded808af55248301633592a27337b192ca2137bf04",
|
||||
"sha256:cca8de381ffca375dd7cbf13f918dd68ca493e9082e7fe3f5827c08e3cfd2432",
|
||||
"sha256:d201b4851a39c1f2303d99f4199974d8e01d48cec7512b59e532979ba6277def",
|
||||
"sha256:d70fee2708e538a7333bca94170da8ab9233e5eae136143e2275f7b2d9bb4c24",
|
||||
"sha256:e0387c4a584394997335375e897b9d63a7a31a1c77482d8b94f9a1be77bcfd08",
|
||||
"sha256:e6755dd3318c0b170d4727db0e310c26e569faa101c7506b6a3e041f16ef8df9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"hashes": [],
|
||||
"version": "==0.16.0"
|
||||
},
|
||||
"bcrypt": {
|
||||
"hashes": [],
|
||||
"version": "==3.1.4"
|
||||
},
|
||||
"blinker": {
|
||||
"hashes": [
|
||||
"sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==1.4"
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [],
|
||||
"version": "==1.11.5"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
|
||||
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==6.7"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:1c0fbb1cba6f809fe3e6b27f8f6d517ca171f848922708871403636143d530d9",
|
||||
"sha256:af77d5d82fa027c032650fb8afdef3cd0a3735ba01480bee908cddad9be1bdce"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"h2": {
|
||||
"hashes": [
|
||||
"sha256:4be613e35caad5680dc48f98f3bf4e7338c7c429e6375a5137be7fbe45219981",
|
||||
"sha256:b2962f883fa392a23cbfcc4ad03c335bcc661be0cf9627657b589f0df2206e64"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"hpack": {
|
||||
"hashes": [
|
||||
"sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89",
|
||||
"sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"hypercorn": {
|
||||
"hashes": [
|
||||
"sha256:4c7cbc92e134d913d23815155fc190c8f52425fc2e0ce8131d192c310a43b91e",
|
||||
"sha256:5a56a2e56f157516ea95fa589ee72d996894efba0530b411752d16e461af62e0"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==0.1.0"
|
||||
},
|
||||
"hyperframe": {
|
||||
"hashes": [
|
||||
"sha256:87567c9eb1540de1e7f48805adf00e87856409342fdebd0cd20cf5d381c38b69",
|
||||
"sha256:a25944539db36d6a2e47689e7915dcee562b3f8d10c6cdfa0d53c91ed692fb04"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==5.1.0"
|
||||
},
|
||||
"itsdangerous": {
|
||||
"hashes": [
|
||||
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==0.24"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==1.0"
|
||||
},
|
||||
"multidict": {
|
||||
"hashes": [
|
||||
"sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0",
|
||||
"sha256:1d6e191965505652f194bc4c40270a842922685918a4f45e6936a6b15cc5816d",
|
||||
"sha256:295961a6a88f1199e19968e15d9b42f3a191c89ec13034dbc212bf9c394c3c82",
|
||||
"sha256:2be5af084de6c3b8e20d6421cb0346378a9c867dcf7c86030d6b0b550f9888e4",
|
||||
"sha256:2eb99617c7a0e9f2b90b64bc1fb742611718618572747d6f3d6532b7b78755ab",
|
||||
"sha256:4ba654c6b5ad1ae4a4d792abeb695b29ce981bb0f157a41d0fd227b385f2bef0",
|
||||
"sha256:5ba766433c30d703f6b2c17eb0b6826c6f898e5f58d89373e235f07764952314",
|
||||
"sha256:a59d58ee85b11f337b54933e8d758b2356fcdcc493248e004c9c5e5d11eedbe4",
|
||||
"sha256:a6e35d28900cf87bcc11e6ca9e474db0099b78f0be0a41d95bef02d49101b5b2",
|
||||
"sha256:b4df7ca9c01018a51e43937eaa41f2f5dce17a6382fda0086403bcb1f5c2cf8e",
|
||||
"sha256:bbd5a6bffd3ba8bfe75b16b5e28af15265538e8be011b0b9fddc7d86a453fd4a",
|
||||
"sha256:d870f399fcd58a1889e93008762a3b9a27cf7ea512818fc6e689f59495648355",
|
||||
"sha256:e9404e2e19e901121c3c5c6cffd5a8ae0d1d67919c970e3b3262231175713068"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==4.3.1"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [],
|
||||
"version": "==2.18"
|
||||
},
|
||||
"quart": {
|
||||
"hashes": [
|
||||
"sha256:a5f64f15ffa5e063c07ad3675c7fe82d3945a4d48c4e3f8d0bda0494c4964a1d",
|
||||
"sha256:d6da4f0e99403918874ad9e7124f13f3c0a1bb4da10021c56a89433527909d52"
|
||||
],
|
||||
"index": "pypi",
|
||||
"hashes": [],
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [],
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"sortedcontainers": {
|
||||
"hashes": [
|
||||
"sha256:607294c6e291a270948420f7ffa1fb3ed47384a4c08db6d1e9c92d08a6981982",
|
||||
"sha256:ef38b128302ee8f65d81e31c9d8fbf10d81df4d6d06c9c0b66f01d33747525bb"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==2.0.4"
|
||||
},
|
||||
"typing": {
|
||||
"hashes": [],
|
||||
"version": "==3.6.4"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:1c0a8e3b4ce55207a03dd0dcb98bc47a704c71f14fe4311ec860cc8af8f4bd27",
|
||||
"sha256:8b0962ecb92847974514b1724c8ae2b6dd1ffe86bcdfac429517f5e583ada658",
|
||||
"sha256:be7b05ddab71727fabf1f071365043cf034e4cdac9cade1f1d61a6cc526aaafe"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==3.6.5"
|
||||
},
|
||||
"wsproto": {
|
||||
"hashes": [
|
||||
"sha256:02f214f6bb43cda62a511e2e8f1d5fa4703ed83d376d18d042bd2bbf2e995824",
|
||||
"sha256:d2a7f718ab3144ec956a3267d57b5c172f0668827f5803e7d670837b0125b9fa"
|
||||
],
|
||||
"hashes": [],
|
||||
"version": "==0.11.0"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
from .gateway import bp as gateway
|
||||
from .auth import bp as auth
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
import random
|
||||
import base64
|
||||
|
||||
import asyncpg
|
||||
import itsdangerous
|
||||
import bcrypt
|
||||
from quart import Blueprint, jsonify, request, current_app as app
|
||||
|
||||
from ..snowflake import get_snowflake
|
||||
from ..errors import AuthError
|
||||
|
||||
|
||||
bp = Blueprint('auth', __name__)
|
||||
|
||||
|
||||
async def hash_data(data: str) -> str:
|
||||
"""Hash information with bcrypt."""
|
||||
data = bytes(data, 'utf-8')
|
||||
|
||||
future = app.loop.run_in_executor(
|
||||
None, bcrypt.hashpw, data, bcrypt.gensalt(14))
|
||||
|
||||
hashed = await future
|
||||
return hashed.decode('utf-8')
|
||||
|
||||
|
||||
async def check_password(pwd_hash, given_password) -> bool:
|
||||
"""Check if a given password matches the given hash."""
|
||||
pwd_hash = pwd_hash.encode('utf-8')
|
||||
given_password = given_password.encode('utf-8')
|
||||
|
||||
future = app.loop.run_in_executor(
|
||||
None, bcrypt.checkpw, pwd_hash, given_password)
|
||||
|
||||
return await future
|
||||
|
||||
|
||||
def make_token(user_id, user_pwd_hash) -> str:
|
||||
"""Generate a single token for a user."""
|
||||
signer = itsdangerous.Signer(user_pwd_hash)
|
||||
user_id = base64.b64encode(str(user_id).encode('utf-8'))
|
||||
|
||||
return signer.sign(user_id).decode('utf-8')
|
||||
|
||||
|
||||
@bp.route('/register', methods=['POST'])
|
||||
async def register():
|
||||
"""Register a user on litecord"""
|
||||
j = await request.get_json()
|
||||
email, password, username = j['email'], j['password'], j['username']
|
||||
|
||||
new_id = get_snowflake()
|
||||
new_discrim = str(random.randint(1, 9999))
|
||||
pwd_hash = await hash_data(password)
|
||||
|
||||
try:
|
||||
await app.db.execute("""
|
||||
INSERT INTO users (id, email, username,
|
||||
discriminator, password_hash)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
""", new_id, email, username, new_discrim, pwd_hash)
|
||||
except asyncpg.UniqueViolationError:
|
||||
raise AuthError('Email already used.')
|
||||
|
||||
return jsonify({
|
||||
'token': make_token(new_id, pwd_hash)
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/login', methods=['POST'])
|
||||
async def login():
|
||||
"""Login one user into Litecord."""
|
||||
j = await request.get_json()
|
||||
email, password = j['email'], j['password']
|
||||
|
||||
row = await app.db.fetchrow("""
|
||||
SELECT id, password_hash
|
||||
FROM users
|
||||
WHERE email = $1
|
||||
""", email)
|
||||
|
||||
if not row:
|
||||
return jsonify({'email': ['User not found.']})
|
||||
|
||||
user_id, pwd_hash = row
|
||||
|
||||
if not await check_password(pwd_hash, password):
|
||||
return jsonify({'password': ['Password does not match.']})
|
||||
|
||||
return jsonify({
|
||||
'token': make_token(user_id, pwd_hash)
|
||||
})
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
class LitecordError(Exception):
|
||||
status_code = 500
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return self.args[0]
|
||||
|
||||
|
||||
class AuthError(LitecordError):
|
||||
status_code = 403
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
"""
|
||||
snowflake.py - snowflake helper functions
|
||||
|
||||
These functions generate discord-like snowflakes.
|
||||
File brought in from
|
||||
litecord-reference(https://github.com/lnmds/litecord-reference)
|
||||
"""
|
||||
import time
|
||||
import hashlib
|
||||
import os
|
||||
import base64
|
||||
|
||||
# encoded in ms
|
||||
EPOCH = 1420070400000
|
||||
|
||||
# internal state
|
||||
_generated_ids = 0
|
||||
PROCESS_ID = 1
|
||||
WORKER_ID = 1
|
||||
|
||||
Snowflake = int
|
||||
|
||||
|
||||
def get_invite_code() -> str:
|
||||
"""Get a random invite code."""
|
||||
random_stuff = hashlib.sha512(os.urandom(1024)).digest()
|
||||
code = base64.urlsafe_b64encode(random_stuff).decode().replace('=', '5') \
|
||||
.replace('_', 'W').replace('-', 'm')
|
||||
return code[:6]
|
||||
|
||||
|
||||
def _snowflake(timestamp: int) -> Snowflake:
|
||||
"""Get a snowflake from a specific timestamp
|
||||
|
||||
This function relies on modifying internal variables
|
||||
to generate unique snowflakes. Because of that every call
|
||||
to this function will generate a different snowflake,
|
||||
even with the same timestamp.
|
||||
|
||||
Arguments
|
||||
---------
|
||||
timestamp: int
|
||||
Timestamp to be feed in to the snowflake algorithm.
|
||||
This timestamp has to be an UNIX timestamp
|
||||
with millisecond precision.
|
||||
"""
|
||||
# Yes, using global variables aren't the best idea
|
||||
# Maybe we could distribute the work of snowflake generation
|
||||
# to actually separated servers? :thinking:
|
||||
global _generated_ids
|
||||
|
||||
# bits 0-12 encode _generated_ids (size 12)
|
||||
genid_b = '{0:012b}'.format(_generated_ids)
|
||||
|
||||
# bits 12-17 encode PROCESS_ID (size 5)
|
||||
procid_b = '{0:05b}'.format(PROCESS_ID)
|
||||
|
||||
# bits 17-22 encode WORKER_ID (size 5)
|
||||
workid_b = '{0:05b}'.format(WORKER_ID)
|
||||
|
||||
# bits 22-64 encode (timestamp - EPOCH) (size 42)
|
||||
epochized = timestamp - EPOCH
|
||||
epoch_b = '{0:042b}'.format(epochized)
|
||||
|
||||
snowflake_b = f'{epoch_b}{workid_b}{procid_b}{genid_b}'
|
||||
_generated_ids += 1
|
||||
|
||||
return int(snowflake_b, 2)
|
||||
|
||||
|
||||
def snowflake_time(snowflake: Snowflake) -> float:
|
||||
"""Get the UNIX timestamp(with millisecond precision, as a float)
|
||||
from a specific snowflake.
|
||||
"""
|
||||
|
||||
# the total size for a snowflake is 64 bits,
|
||||
# considering it is a string, position 0 to 42 will give us
|
||||
# the `epochized` variable
|
||||
snowflake_b = '{0:064b}'.format(snowflake)
|
||||
epochized_b = snowflake_b[:42]
|
||||
epochized = int(epochized_b, 2)
|
||||
|
||||
# since epochized is the time *since* the EPOCH
|
||||
# the unix timestamp will be the time *plus* the EPOCH
|
||||
timestamp = epochized + EPOCH
|
||||
|
||||
# convert it to seconds
|
||||
# since we don't want to break the entire
|
||||
# snowflake interface
|
||||
return timestamp / 1000
|
||||
|
||||
|
||||
def get_snowflake():
|
||||
"""Generate a snowflake"""
|
||||
return _snowflake(int(time.time() * 1000))
|
||||
37
run.py
37
run.py
|
|
@ -1,10 +1,13 @@
|
|||
import logging
|
||||
import asyncio
|
||||
|
||||
import asyncpg
|
||||
from quart import Quart, g, Blueprint
|
||||
from quart import Quart, g, jsonify
|
||||
|
||||
import config
|
||||
from litecord.blueprints import gateway
|
||||
|
||||
from litecord.blueprints import gateway, auth
|
||||
from litecord.errors import LitecordError
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -18,20 +21,44 @@ def make_app():
|
|||
|
||||
app = make_app()
|
||||
app.register_blueprint(gateway, url_prefix='/api/v6')
|
||||
app.register_blueprint(auth, url_prefix='/api/v6')
|
||||
|
||||
|
||||
@app.before_serving
|
||||
async def app_before_serving():
|
||||
log.info('opening db')
|
||||
app.db_pool = await asyncpg.create_pool(**app.config['POSTGRES'])
|
||||
app.db = await asyncpg.create_pool(**app.config['POSTGRES'])
|
||||
g.app = app
|
||||
|
||||
app.loop = asyncio.get_event_loop()
|
||||
g.loop = asyncio.get_event_loop()
|
||||
|
||||
|
||||
@app.after_serving
|
||||
async def app_after_serving():
|
||||
log.info('closing db')
|
||||
await app.db_pool.close()
|
||||
await app.db.close()
|
||||
|
||||
|
||||
@app.errorhandler(LitecordError)
|
||||
async def handle_litecord_err(err):
|
||||
return jsonify({
|
||||
'error': True,
|
||||
# 'code': err.code,
|
||||
'status': err.status_code,
|
||||
'message': err.message,
|
||||
}), err.status_code
|
||||
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def handle_exception(err):
|
||||
log.exception('Error happened in the app')
|
||||
return jsonify({
|
||||
'error': True,
|
||||
'message': repr(err)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/')
|
||||
async def index():
|
||||
return 'hai'
|
||||
return 'hewwo'
|
||||
|
|
|
|||
|
|
@ -57,13 +57,13 @@ CREATE TABLE IF NOT EXISTS users (
|
|||
id bigint UNIQUE NOT NULL,
|
||||
username varchar(32) NOT NULL,
|
||||
discriminator varchar(4) NOT NULL,
|
||||
avatar bigint REFERENCES files (id),
|
||||
email varchar(255) NOT NULL UNIQUE,
|
||||
|
||||
-- user properties
|
||||
bot boolean DEFAULT FALSE,
|
||||
mfa_enabled boolean DEFAULT FALSE,
|
||||
verified boolean DEFAULT FALSE,
|
||||
email varchar(255) NOT NULL,
|
||||
avatar bigint REFERENCES files (id) DEFAULT NULL,
|
||||
|
||||
-- user badges, discord dev, etc
|
||||
flags int DEFAULT 0,
|
||||
|
|
@ -74,7 +74,6 @@ CREATE TABLE IF NOT EXISTS users (
|
|||
-- private info
|
||||
phone varchar(60) DEFAULT '',
|
||||
password_hash text NOT NULL,
|
||||
password_salt text NOT NULL,
|
||||
|
||||
PRIMARY KEY (id, username, discriminator)
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue