...
 
Commits (13)
......@@ -19,6 +19,7 @@ from api.common.email import (
from api.bp.profile import get_limits, delete_user
from api.bp.admin.audit_log_actions.user import UserEditAction, UserDeleteAction
from api.response import resp_empty
log = logging.getLogger(__name__)
bp = Blueprint(__name__)
......@@ -95,10 +96,8 @@ async def activate_user(request, admin_id, user_id: int):
await request.app.storage.invalidate(user_id, 'active')
await notify_activate(request.app, user_id)
return response.json({
'success': True,
'result': result,
})
# returning resp_empty instead of the result as it's practically useless.
return resp_empty()
@bp.post('/api/admin/activate_email/<user_id:int>')
......@@ -171,10 +170,7 @@ async def deactivate_user(request, admin_id: int, user_id: int):
await request.app.storage.invalidate(user_id, 'active')
return response.json({
'success': True,
'result': result
})
return resp_empty()
@bp.get('/api/admin/users/search')
......@@ -225,89 +221,6 @@ async def users_search(request, admin_id):
})
# === DEPRECATED ===
# read https://gitlab.com/elixire/elixire/issues/61#note_91039503
# These routes are here to maintain compatibility with some of our
# utility software (admin panels)
@bp.get('/api/admin/listusers/<page:int>')
@admin_route
async def list_users_handler(request, admin_id, page: int):
"""List users in the service"""
data = await request.app.db.fetch("""
SELECT user_id, username, active, admin, domain,
subdomain, email, paranoid, consented
FROM users
ORDER BY user_id ASC
LIMIT 20
OFFSET ($1 * 20)
""", page)
def _cnv(row):
drow = dict(row)
drow['user_id'] = str(row['user_id'])
return drow
return response.json(list(map(_cnv, data)))
@bp.get('/api/admin/list_inactive/<page:int>')
@admin_route
async def inactive_users_handler(request, admin_id, page: int):
data = await request.app.db.fetch("""
SELECT user_id, username, active, admin, domain, subdomain,
email, paranoid, consented
FROM users
WHERE active=false
ORDER BY user_id ASC
LIMIT 20
OFFSET ($1 * 20)
""", page)
def _cnv(row):
drow = dict(row)
drow['user_id'] = str(row['user_id'])
return drow
return response.json(list(map(_cnv, data)))
@bp.post('/api/admin/search/user/<page:int>')
async def search_user(request, user_id: int, page: int):
"""Search a user by pattern matching the username."""
try:
pattern = str(request.json['search_term'])
except (KeyError, TypeError, ValueError):
raise BadInput('Invalid search_term')
if not pattern:
raise BadInput('Insert a pattern.')
pattern = f'%{pattern}%'
rows = await request.app.db.fetch("""
SELECT user_id, username, active, admin, consented
FROM users
WHERE username LIKE $1 OR user_id::text LIKE $1
ORDER BY user_id ASC
LIMIT 20
OFFSET ($2 * 20)
""", pattern, page)
res = []
for row in rows:
drow = dict(row)
drow['user_id'] = str(drow['user_id'])
res.append(drow)
return response.json(res)
# === END DEPRECATED ===
async def _pu_check(db, db_name,
user_id, payload, updated_fields, field, col=None):
"""Checks if the given field exists on payload.
......@@ -399,6 +312,4 @@ async def del_user(request, admin_id, user_id):
async with UserDeleteAction(request, user_id):
await delete_user(request.app, user_id, True)
return response.json({
'success': True
})
return resp_empty()
......@@ -5,6 +5,7 @@
from sanic import Blueprint
from sanic import response
from api.response import resp_empty
from ..common import TokenType
from ..common.auth import login_user, gen_token, pwd_hash
from ..schema import validate, REVOKE_SCHEMA
......@@ -66,6 +67,4 @@ async def revoke_handler(request):
await request.app.storage.invalidate(user['user_id'], 'password_hash')
return response.json({
'success': True
})
return resp_empty()
......@@ -8,6 +8,7 @@ import logging
from sanic import Blueprint
from sanic import response
from api.response import resp_empty
from ..common import delete_file, delete_shorten
from ..common.auth import token_check, password_check
from ..decorators import auth_route
......@@ -112,19 +113,6 @@ async def list_handler(request):
})
@bp.delete('/api/delete')
async def delete_handler(request):
"""Invalidate a file."""
user_id = await token_check(request)
file_name = str(request.json['filename'])
await delete_file(request.app, file_name, user_id)
return response.json({
'success': True
})
@bp.post('/api/delete_all')
@auth_route
async def delete_all(request, user_id):
......@@ -144,28 +132,21 @@ async def delete_all(request, user_id):
f'delete_files_{user_id}'
)
return response.json({
'success': True,
})
return resp_empty()
@bp.route('/api/delete/<shortname>', methods=['GET', 'DELETE'])
@bp.delete('/api/files/<shortname>')
@bp.get('/api/files/<shortname>/delete')
@auth_route
async def delete_single(request, user_id, shortname):
"""Delete a single file."""
await delete_file(request.app, shortname, user_id)
return response.json({
'success': True
})
return resp_empty()
@bp.delete('/api/shortendelete')
async def shortendelete_handler(request):
@bp.delete('/api/shortens/<shorten_name>')
@auth_route
async def shortendelete_handler(request, user_id, shorten_name):
"""Invalidate a shorten."""
user_id = await token_check(request)
file_name = str(request.json['filename'])
await delete_shorten(request.app, file_name, user_id)
return response.json({
'success': True
})
await delete_shorten(request.app, shorten_name, user_id)
return resp_empty()
......@@ -2,18 +2,25 @@
# Copyright 2018-2019, elixi.re Team and the elixire contributors
# SPDX-License-Identifier: AGPL-3.0-only
"""
elixire - misc routes
"""
import datetime
from sanic import Blueprint, response
from ..version import VERSION, API_VERSION
from api.version import VERSION, API_VERSION
bp = Blueprint('misc')
def _owo(string: str) -> str:
return string.replace('0', '0w0').replace('r', 'w')
def _make_feature_list(cfg):
res = []
if cfg.UPLOADS_ENABLED:
res.append('uploads')
elif cfg.SHORTENS_ENABLED:
res.append('shortens')
elif cfg.REGISTRATIONS_ENABLED:
res.append('registrations')
elif cfg.PATCH_API_PROFILE_ENABLED:
res.append('pfupdate')
return res
@bp.get('/api/hello')
......@@ -31,50 +38,5 @@ async def hello_route(request):
'ip_ban_period': cfg.IP_BAN_PERIOD,
'rl_threshold': cfg.RL_THRESHOLD,
'accepted_mimes': cfg.ACCEPTED_MIMES,
})
@bp.get('/api/hewwo')
async def h_hewwo(request):
"""owo"""
return response.json({
'name': _owo(request.app.econfig.INSTANCE_NAME),
'version': _owo(VERSION),
'api': _owo(API_VERSION),
})
@bp.get('/api/science')
async def science_route(request):
"""*insert b4nzyblob*"""
return response.text("Hewoo! We'we nyot discowd we don't spy on nyou :3")
@bp.get('/api/boron')
async def ipek_yolu(request):
"""calculates days until 100th year anniversary of treaty of lausanne"""
world_power_deadline = datetime.date(2023, 7, 24)
days_to_wp = (world_power_deadline - datetime.date.today()).days
is_world_power = (days_to_wp <= 0)
return response.json({
'world_power': is_world_power,
'days_until_world_power': days_to_wp
})
@bp.get('/api/features')
async def fetch_features(request):
"""Fetch instance features.
So that the frontend can e.g disable the
register button when the instance's registration enabled
flag is set to false.
"""
cfg = request.app.econfig
return response.json({
'uploads': cfg.UPLOADS_ENABLED,
'shortens': cfg.SHORTENS_ENABLED,
'registrations': cfg.REGISTRATIONS_ENABLED,
'pfupdate': cfg.PATCH_API_PROFILE_ENABLED,
'features': _make_feature_list(cfg)
})
......@@ -9,6 +9,7 @@ import asyncpg
from sanic import Blueprint, response
from api.response import resp_empty
from ..errors import FailedAuth, FeatureDisabled, BadInput
from ..common.auth import token_check, password_check, pwd_hash,\
check_admin, check_domain_id
......@@ -441,9 +442,7 @@ async def deactivate_user_from_email(request):
log.warning(f'Deactivated user ID {user_id} by request.')
return response.json({
'success': True
})
return resp_empty()
@bp.post('/api/reset_password')
......@@ -517,6 +516,4 @@ async def password_reset_confirmation(request):
await _update_password(request, user_id, new_pwd)
await clean_etoken(app, token, 'email_pwd_reset_tokens')
return response.json({
'success': True
})
return resp_empty()
......@@ -33,46 +33,27 @@ async def get_domain_info(db, domain_id) -> dict:
stats = {}
stats['users'] = await db.fetchval("""
SELECT COUNT(*)
FROM users
WHERE domain = $1
# doing batch queries should help us speed up the overall request time
rows = await db.fetchrow("""
SELECT
(SELECT COUNT(*) FROM users WHERE domain = $1),
(SELECT COUNT(*) FROM shortens WHERE domain = $1)
""", domain_id)
stats['files'] = await db.fetchval("""
SELECT COUNT(*)
FROM files
WHERE domain = $1
""", domain_id)
stats['users'] = rows[0]
stats['shortens'] = rows[1]
filestats = await _domain_file_stats(db, domain_id, ignore_consented=True)
stats['files'], stats['size'] = filestats
stats['shortens'] = await db.fetchval("""
SELECT COUNT(*)
FROM shortens
WHERE domain = $1
""", domain_id)
owner_id = await db.fetchval("""
SELECT user_id
FROM domain_owners
WHERE domain_id = $1
""", domain_id)
owner_data = await db.fetchrow("""
SELECT username, active, consented, admin
SELECT user_id::text, username, active, consented, admin
FROM users
WHERE user_id = $1
""", owner_id)
WHERE user_id = (SELECT user_id FROM domain_owners WHERE domain_id = $1)
""", domain_id)
if owner_data:
downer = {
**dict(owner_data),
**{
'user_id': str(owner_id)
}
}
downer = dict(owner_data)
else:
downer = None
......@@ -89,21 +70,19 @@ async def get_domain_public(db, domain_id) -> dict:
"""Get public information about a domain."""
public_stats = {}
public_stats['users'] = await db.fetchval("""
SELECT COUNT(*)
FROM users
WHERE domain = $1 AND consented = true
rows = await db.fetchrow("""
SELECT
(SELECT COUNT(*) FROM users
WHERE domain = $1 AND consented = true),
(SELECT COUNT(*) FROM shortens
JOIN users ON users.user_id = shortens.uploader
WHERE shortens.domain = $1 AND users.consented = true)
""", domain_id)
public_stats['users'] = rows[0]
public_stats['shortens'] = rows[1]
filestats = await _domain_file_stats(db, domain_id)
public_stats['files'], public_stats['size'] = filestats
public_stats['shortens'] = await db.fetchval("""
SELECT COUNT(*)
FROM shortens
JOIN users
ON users.user_id = shortens.uploader
WHERE shortens.domain = $1 AND users.consented = true
""", domain_id)
return public_stats
# elixire: Image Host software
# Copyright 2018-2019, elixi.re Team and the elixire contributors
# SPDX-License-Identifier: AGPL-3.0-only
from sanic import response
def resp_empty():
"""Return an empty response, with 204."""
return response.text('', status=204)
......@@ -78,11 +78,7 @@ async def test_user_activate_cycle(test_cli):
'Authorization': atoken
})
assert resp.status == 200
rjson = await resp.json()
assert isinstance(rjson, dict)
assert rjson['success']
assert resp.status == 204
# check profile for deactivation
resp = await test_cli.get(f'/api/admin/users/{uid}', headers={
......@@ -98,11 +94,7 @@ async def test_user_activate_cycle(test_cli):
resp = await test_cli.post(f'/api/admin/activate/{uid}', headers={
'Authorization': atoken
})
assert resp.status == 200
rjson = await resp.json()
assert isinstance(rjson, dict)
assert rjson['success']
assert resp.status == 204
# check profile
resp = await test_cli.get(f'/api/admin/users/{uid}', headers={
......@@ -153,6 +145,11 @@ async def test_domain_stats(test_cli):
# not the best data validation...
assert isinstance(rjson, dict)
for domain in rjson.values():
assert isinstance(domain, dict)
assert isinstance(domain['info'], dict)
assert isinstance(domain['stats'], dict)
assert isinstance(domain['public_stats'], dict)
async def test_domain_patch(test_cli):
......
......@@ -70,21 +70,21 @@ async def test_valid_token(test_cli):
async def test_revoke(test_cli):
token = await login_normal(test_cli)
response_valid = await test_cli.get('/api/profile', headers={
resp = await test_cli.get('/api/profile', headers={
'Authorization': token,
})
assert response_valid.status == 200
assert resp.status == 200
revoke_call = await test_cli.post('/api/revoke', json={
resp = await test_cli.post('/api/revoke', json={
'user': USERNAME,
'password': PASSWORD
})
assert revoke_call.status == 200
assert resp.status == 204
response_invalid = await test_cli.get('/api/profile', headers={
resp = await test_cli.get('/api/profile', headers={
'Authorization': token,
})
assert response_invalid.status == 403
assert resp.status == 403
......@@ -3,21 +3,19 @@
# SPDX-License-Identifier: AGPL-3.0-only
async def test_api(test_cli):
async def test_hello(test_cli):
"""Test basic route"""
response = await test_cli.get('/api/hello')
assert response.status == 200
resp_json = await response.json()
assert isinstance(resp_json['name'], str)
assert isinstance(resp_json['version'], str)
rjson = await response.json()
assert isinstance(rjson['name'], str)
assert isinstance(rjson['version'], str)
assert isinstance(rjson['api'], str)
assert isinstance(rjson['support_email'], str)
assert isinstance(rjson['ban_period'], str)
assert isinstance(rjson['ip_ban_period'], str)
assert isinstance(rjson['rl_threshold'], int)
async def test_api_features(test_cli):
resp = await test_cli.get('/api/features')
assert resp.status == 200
rjson = await resp.json()
assert isinstance(rjson['uploads'], bool)
assert isinstance(rjson['shortens'], bool)
assert isinstance(rjson['registrations'], bool)
assert isinstance(rjson['pfupdate'], bool)
assert isinstance(rjson['accepted_mimes'], list)
assert isinstance(rjson['features'], list)
......@@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
import aiohttp
import secrets
from .common import login_normal, png_data
......@@ -63,26 +64,31 @@ async def test_delete_file(test_cli):
await check_exists(test_cli, respjson['shortname'], utoken)
# test delete
resp_del = await test_cli.delete('/api/delete', headers={
short = respjson['shortname']
resp_del = await test_cli.delete(f'/api/files/{short}', headers={
'Authorization': utoken
}, json={
'filename': respjson['shortname']
})
assert resp_del.status == 200
rdel_json = await resp_del.json()
assert isinstance(rdel_json, dict)
assert rdel_json['success']
assert resp_del.status == 204
await check_exists(test_cli, respjson['shortname'], utoken, True)
async def test_delete_nonexist(test_cli):
"""Test deletions of files that don't exist."""
utoken = await login_normal(test_cli)
resp_del = await test_cli.delete('/api/delete', headers={
rand_file = secrets.token_urlsafe(20)
resp_del = await test_cli.delete(f'/api/files/{rand_file}', headers={
'Authorization': utoken
})
assert resp_del.status == 404
# ensure sharex compatibility endpoint works too
resp_del = await test_cli.get(f'/api/files/{rand_file}/delete', headers={
'Authorization': utoken
}, json={
'filename': 'lkdjklfjkgkghkkhsfklhjslkdfjglakdfjl'
})
assert resp_del.status == 404