async def follow(username: str): request: Request = quart_rq async with g.pool.acquire() as con: con: asyncpg.Connection = con if 'Authorization' not in request.headers: return no('Missing authorization.') user_id = await con.fetchval( 'SELECT user_id FROM sessions WHERE token = $1', request.headers.get('Authorization')) if not user_id: return no('Invalid authorization.') following_user_id = await con.fetchval( 'SELECT id FROM users WHERE username_lower = $1', username.lower()) if not following_user_id: return no('User not found.') await con.execute( 'INSERT INTO following (id, follower_user_id, following_user_id) ' + 'VALUES ($1, $2, $3) ON CONFLICT (follower_user_id, ' + 'following_user_id) DO NOTHING', uuid4(), user_id, following_user_id) return {'success': True, 'message': ''}
async def unfollow(username: str): request: Request = quart_rq async with g.pool.acquire() as con: con: asyncpg.Connection = con if 'Authorization' not in request.headers: return no('Missing authorization.') user_id = await con.fetchval( 'SELECT user_id FROM sessions WHERE token = $1', request.headers.get('Authorization')) if not user_id: return no('Invalid authorization.') following_user_id = await con.fetchval( 'SELECT id FROM users WHERE username_lower = $1', username.lower()) if not following_user_id: return no('User not found.') await con.execute( 'DELETE FROM following WHERE ' + 'follower_user_id = $1 AND following_user_id = $2', user_id, following_user_id) return {'success': True, 'message': ''}
async def rep(username: str): request: Request = quart_rq json = await request.get_json() if 'amount' not in json: return no('Missing amount.') if 'message' not in json: return no('Missing message') amount = json['amount'] message = json['message'] if not isinstance(amount, int): return no('Invalid amount.') if len(message) > 255: return no('Message cannot be more than 255 characters.') async with g.pool.acquire() as con: con: asyncpg.Connection = con if 'Authorization' not in request.headers: return no('Missing authorization.') user = await con.fetchrow( 'SELECT user_id as id, u.admin as admin FROM sessions ' + 'INNER JOIN users u on u.id = sessions.user_id WHERE token = $1', request.headers.get('Authorization')) if not user: return no('Invalid authorization.') if abs(amount) > 1 and not user['admin']: return no('Invalid amount.') target_user_id = await con.fetchval( 'SELECT id FROM users WHERE username_lower = $1', username.lower()) if not target_user_id: return no('User not found.') if user['id'] == target_user_id: return no('You cannot give yourself reputation.') await con.execute( 'DELETE FROM reputation_log WHERE ' + 'from_user_id = $1 AND to_user_id = $2', user['id'], target_user_id) if amount != 0: await con.execute( 'INSERT INTO reputation_log (id, ' + 'from_user_id, to_user_id, amount, message) ' + 'VALUES ($1, $2, $3, $4, $5)', uuid4(), user['id'], target_user_id, amount, message) return {'success': True, 'message': ''}
async def list_users(): request: Request = quart_rq category = 'rep' if 'c' in request.args: category = request.args['c'] if category not in QUERIES.keys(): category = 'rep' query = LIST_USERS_BASE_QUERY + ' ' + QUERIES[category] + ' LIMIT 50' query_args = [] users = [] async with g.pool.acquire() as con: con: asyncpg.Connection = con user_id = await con.fetchval( 'SELECT user_id FROM sessions WHERE token = $1', request.headers.get('Authorization')) if category == 'fol' or category == 'fpr': if 'Authorization' not in request.headers: return no('missing_authorization') elif not user_id: return no('invalid_authorization') else: query_args = [user_id] rows = await con.fetch(query, *query_args) for row in rows: description = row['description'] if len(description) > 32: description = description[:29] + '...' users.append({ 'username': row['username'], 'fullName': row['full_name'], 'admin': row['admin'], 'partialDescription': description, 'reputation': row['reputation'], 'createdAt': row['created_at'].isoformat() }) return jsonify(users)
async def logout(): if 'Authorization' not in request.headers: return no('Missing authorization.') async with g.pool.acquire() as con: con: asyncpg.Connection = con await con.execute('DELETE FROM sessions WHERE token = $1', request.headers.get('Authorization')) return {'success': True, 'message': ''}
async def me_avatar(): request: Request = quart_rq async with g.pool.acquire() as con: con: asyncpg.Connection = con if 'Authorization' not in request.headers: return no('Missing authorization.') user_id = await con.fetchval( 'SELECT user_id FROM sessions WHERE token = $1', request.headers.get('Authorization')) if not user_id: return no('Invalid authorization.') files = await request.files if 'avatar' not in files: return no('Missing avatar') avatar = files['avatar'] avatar.save('avatartemp/' + str(user_id)) with open('avatartemp/' + str(user_id), 'rb') as file: file_bytes = file.read() md5sum = md5(file_bytes).hexdigest() path = g.config['avatar_dir'] + '/' + md5sum if not isfile(path): with open(path, 'wb') as avatar_file: avatar_file.write(file_bytes) remove('avatartemp/' + str(user_id), ) url = 'https://static.craftjobs.net/avatars/' + md5sum await con.execute('UPDATE users SET avatar_url = $1 WHERE id = $2', url, user_id) return {'success': True, 'message': url}
async def root(): json = await request.get_json() if 'username' not in json: return no('Missing username.') if 'password' not in json: return no('Missing password.') if 'remember' not in json: return no('Missing remember.') username = json['username'] password = json['password'] remember = bool(json['remember']) is_email = '@' in username column = 'email' if is_email else 'username_lower' async with g.pool.acquire() as con: con: asyncpg.Connection = con user = await con.fetchrow( 'SELECT id, password_hashed FROM users ' + f'WHERE {column} = $1', username.lower()) if not user: return no('Invalid username/email or password.') if not bcrypt.checkpw(password.encode('utf-8'), user['password_hashed'].encode('utf-8')): return no('Invalid username/email or password.') token = token_hex(32) await con.execute( 'INSERT INTO sessions (id, token, user_id, expires_at) ' + 'VALUES ($1, $2, $3, $4)', uuid4(), token, user['id'], datetime.now() + timedelta(days=30 if remember else 1)) return {'success': True, 'token': token}
async def get_user(username: str): request: Request = quart_rq self_only = 's' in request.args async with g.pool.acquire() as con: con: asyncpg.Connection = con db_user = await con.fetchrow( USER_SELECT_QUERY if not self_only else USER_BASIC_SELECT_QUERY, username.lower()) authed_as = None self = {} if 'Authorization' in request.headers: db_self = await con.fetchrow(USER_BY_SESSION_QUERY, request.headers.get('Authorization')) if db_self: authed_as = db_self['id'] self['username'] = db_self['username'] self['admin'] = db_self['admin'] self['isSelf'] = db_self['username'].lower() == username.lower( ) self['isFollowing'] = False self['reputationGiven'] = 0 self['plusReputationFollowing'] = [] self['minusReputationFollowing'] = [] if not db_user: return no('not_found') user_id = db_user['id'] if not self_only: rate_lower = db_user['rate_lower'] rate_higher = db_user['rate_higher'] rate_range = [] if rate_lower != -1: rate_range.append(rate_lower) if rate_higher != -1: rate_range.append(rate_higher) rep_log = await con.fetch(REP_LOG_QUERY, user_id) if not self_only: total_rep = 0 formatted_rep_log = [] rep_amount_by_user = {} for rep_entry in rep_log: if not self_only: total_rep += rep_entry['amount'] formatted_rep_log.append({ 'user': rep_entry['from_username'], 'amount': rep_entry['amount'], 'time': rep_entry['time'].isoformat(), 'message': rep_entry['message'], 'userFullName': rep_entry['from_full_name'] }) rep_amount_by_user[rep_entry['from_user_id']] = { 'amount': rep_entry['amount'], 'user': rep_entry['from_username'] } if authed_as and rep_entry['from_user_id'] == authed_as: self['reputationGiven'] = rep_entry['amount'] if authed_as: db_following = await con.fetch( 'SELECT following_user_id FROM following ' + 'WHERE follower_user_id = $1', authed_as) for follow_row in db_following: following_id = follow_row['following_user_id'] if following_id == user_id: self['isFollowing'] = True if following_id in rep_amount_by_user: following_rep = rep_amount_by_user[following_id] if following_rep['amount'] > 0: self['plusReputationFollowing'].append( following_rep['user']) else: self['minusReputationFollowing'].append( following_rep['user']) if not self_only: connections = {} db_connections = await con.fetch( 'SELECT connection_type, link FROM connections WHERE ' + 'user_id = $1', user_id) for connection_row in db_connections: con_type = connection_row['connection_type'] link = connection_row['link'] if con_type == 'EMAIL' and not authed_as: link = '_unauthed' connections[con_type] = link april_fools = g.config['april_fools'] shrek = 'https://static.craftjobs.net/TheOneTrueAvatar.jpg' user = { 'avatarUrl': shrek if april_fools else db_user['avatar_url'], 'fullName': db_user['full_name'], 'username': db_user['username'], 'lookingFor': [], # TODO 'rateRange': rate_range, 'reputation': total_rep, 'role': db_user['role'], 'experience': [], # TODO 'rateRangeType': db_user['rate_range_type'], 'languages': [], # TODO 'admin': True if april_fools else db_user['admin'], 'description': db_user['description'], 'reputationLog': formatted_rep_log, 'connections': connections, } ret = {'success': True} if authed_as: ret['self'] = self if not self_only: ret['user'] = user return ret
async def edit_me(): request: Request = quart_rq json: Dict = await request.get_json() if 'Authorization' not in request.headers: return no('Missing authorization.') async with g.pool.acquire() as con: con: asyncpg.Connection = con user_id = await con.fetchval( 'SELECT user_id FROM sessions WHERE token = $1', request.headers.get('Authorization')) if not user_id: return no('Invalid authorization.') main_query = 'UPDATE users SET ' set_idx = 1 set_args = [] for key, value in json.items(): str_value: str = value if key == 'fullName': main_query += f'full_name = ${set_idx}, ' if len(str_value) > 32: return no('Full name must be less than 32 characters.') set_args.append(str_value) set_idx += 1 elif key == 'rateRange': intl_value: List[int] = value last_value = -1 if len(intl_value) > 2: return no('Invalid rate range') for rate in intl_value: if rate > 32767 or rate < 0: return no('Rates must be between 0 and 32767.') if rate < last_value: return no( 'End of rate range must not be less than start.') last_value = rate rate_lower = -1 if len(intl_value) < 1 else intl_value[0] rate_higher = -1 if len(intl_value) < 2 else intl_value[1] main_query += f'rate_lower = ${set_idx}, rate_higher = ${set_idx + 1}, ' set_args.append(rate_lower) set_args.append(rate_higher) set_idx += 2 elif key == 'role': if str_value not in ROLES: return no('Invalid role.') main_query += f'role = ${set_idx}, ' set_args.append(str_value) set_idx += 1 elif key == 'rateRangeType': if str_value not in RATE_RANGE_TYPES: return no('Invalid rate range type.') main_query += f'rate_range_type = ${set_idx}, ' set_args.append(str_value) set_idx += 1 elif key == 'description': if len(str_value) > 2000: return no( 'Description cannot be greater than 2000 characters.') main_query += f'description = ${set_idx}, ' set_args.append(str_value) set_idx += 1 elif key == 'connections': strd_value: Dict[str, str] = value for con_key, con_val in strd_value.items(): if con_key not in CONNECTION_VALIDATORS: return no('Invalid connection: ' + con_key + '.') if con_val == '': await con.execute( 'DELETE FROM connections WHERE ' + 'connection_type = $1 ' + 'AND user_id = $2', con_key, user_id) else: if not CONNECTION_VALIDATORS[con_key](con_val): return no('Invalid ' + con_key.lower()) await con.execute( 'INSERT INTO connections (id, ' + 'user_id, connection_type, link) ' + 'VALUES ($1, $2, $3, $4) ON ' + 'CONFLICT (user_id, ' + 'connection_type) DO UPDATE SET ' + 'link = $4', uuid4(), user_id, con_key, con_val) # No additions if main_query[-4:] == 'SET ': return { 'success': True, 'message': 'Look ma, I\'m a reverse engineer!' } main_query = main_query[:-2] # remove last comma and space main_query += ' WHERE id = $' + str(set_idx) set_args.append(user_id) await con.execute(main_query, *set_args) return {'success': True, 'message': 'Look ma, I\'m a reverse engineer!'}
async def change_password(): # Two potential forms of authentication here. # We use a prefix in the authorization header to determine it. # # Authorization: PasswordReset ... <- password reset, this serves as enough # auth itself # Authorization: Bearer ... <- generic password change, needs valid # oldPassword if 'Authorization' not in request.headers: return no('Missing authorization.') auth_parts = request.headers.get('Authorization').split(' ') json = await request.get_json() if 'password' not in json: return no('Missing password.') if len(auth_parts) != 2: return no('Invalid authorization structure.') auth_type = auth_parts[0] auth_token = auth_parts[1] is_bearer = auth_type == 'Bearer' is_reset = auth_type == 'PasswordReset' if not is_bearer and not is_reset: return no('Invalid authorization type.') async with g.pool.acquire() as con: con: asyncpg.Connection = con if is_bearer: if 'oldPassword' not in json: return no('Missing oldPassword.') user = await con.fetchrow(GET_PASSWORD_AND_ID_BY_SESSION_QUERY, auth_token) if not user: return no('Invalid authorization.') if not bcrypt.checkpw(json['oldPassword'].encode('utf-8'), user['password_hashed'].encode('utf-8')): return no('Old password is incorrect.') user_id = user['id'] field = 'id' else: email = await g.redis.get('login:password_reset:' + auth_token) if not email: return no('Invalid authorization.') user_id = email.decode('utf-8') field = 'email' hashed = bcrypt.hashpw(json['password'].encode('utf-8'), bcrypt.gensalt()).decode('utf-8') user_id = await con.fetchval('UPDATE users SET password_hashed = $1 ' + f'WHERE {field} = $2 RETURNING id', hashed, user_id) await con.execute('DELETE FROM sessions WHERE user_id = $1', user_id) return {'success': True, 'message': ''}