...
 
Commits (15)
Subproject commit 2d906f5b2f95071b876202e1fbc219b6136eb753
Subproject commit d1256e27fa2b5f752b8a80109dc56d4028d215c8
......@@ -45,6 +45,19 @@ async def get_user_handler(request, admin_id, user_id: int):
return response.json(dudata)
@bp.get('/api/admin/users/by-username/<username>')
@admin_route
async def get_user_by_username(request, _admin_id: int, username: str):
"""Get a user object via their username instead of user ID."""
user_id = await request.app.db.fetchval("""
SELECT user_id
FROM users
WHERE username = $1
""", username)
return await get_user_handler(request, user_id)
async def notify_activate(app, user_id: int):
"""Inform user that they got an account."""
if not app.econfig.NOTIFY_ACTIVATION_EMAILS:
......@@ -182,7 +195,6 @@ async def deactivate_user(request, admin_id: int, user_id: int):
async def users_search(request, admin_id):
"""New, revamped search endpoint."""
args = request.raw_args
active = args.get('active', True) != 'false'
query = request.raw_args.get('query')
page = int(args.get('page', 0))
per_page = int(args.get('per_page', 20))
......@@ -193,19 +205,31 @@ async def users_search(request, admin_id):
if per_page < 1:
raise BadInput('Invalid per_page number')
# default to TRUE so the query parses correctly, instead of giving empty
# string
active_query = 'TRUE'
active = args.get('active')
query_args = []
if active is not None:
active_query = 'active = $3'
active = active != 'false'
query_args = [active]
users = await request.app.db.fetch(f"""
SELECT user_id, username, active, admin, consented,
COUNT(*) OVER() as total_count
FROM users
WHERE active = $1
WHERE
{active_query}
AND (
$3 = ''
OR (username LIKE '%'||$3||'%' OR user_id::text LIKE '%'||$3||'%')
$2 = ''
OR (username LIKE '%'||$2||'%' OR user_id::text LIKE '%'||$2||'%')
)
ORDER BY user_id ASC
LIMIT {per_page}
OFFSET ($2 * {per_page})
""", active, page, query or '')
OFFSET ($1 * {per_page})
""", page, query or '', *query_args)
def map_user(record):
row = dict(record)
......
......@@ -85,20 +85,16 @@ async def request_data_dump(request):
})
@bp.get('/api/dump/status')
async def data_dump_user_status(request):
"""Give information about the current dump for the user,
if one exists."""
user_id = await token_check(request)
row = await request.app.db.fetchrow("""
async def get_dump_status(db, user_id: int):
"""Get datadump status."""
row = await db.fetchrow("""
SELECT user_id, start_timestamp, current_id, total_files, files_done
FROM current_dump_state
WHERE user_id = $1
""", user_id)
if not row:
queue = await request.app.db.fetch("""
queue = await db.fetch("""
SELECT user_id
FROM dump_queue
ORDER BY request_timestamp ASC
......@@ -108,22 +104,33 @@ async def data_dump_user_status(request):
try:
pos = queue.index(user_id)
return response.json({
return {
'state': 'in_queue',
'position': pos + 1,
})
}
except ValueError:
return response.json({
return {
'state': 'not_in_queue'
})
}
return response.json({
return {
'state': 'processing',
'start_timestamp': row['start_timestamp'].isoformat(),
'current_id': str(row['current_id']),
'total_files': row['total_files'],
'files_done': row['files_done']
})
}
@bp.get('/api/dump/status')
async def data_dump_user_status(request):
"""Give information about the current dump for the user,
if one exists."""
user_id = await token_check(request)
return response.json(
await get_dump_status(request.app.db, user_id)
)
@bp.get('/api/admin/dump_status')
......
......@@ -19,6 +19,9 @@ from ..schema import validate, PROFILE_SCHEMA, DEACTIVATE_USER_SCHEMA, \
from ..common import delete_file
from ..common.utils import int_
from api.bp.personal_stats import get_counts
from api.bp.datadump.bp import get_dump_status
bp = Blueprint('profile')
log = logging.getLogger(__name__)
......@@ -95,6 +98,12 @@ async def profile_handler(request):
duser['user_id'] = str(duser['user_id'])
duser['limits'] = limits
counts = await get_counts(request.app.db, user_id)
duser['stats'] = counts
dump_status = await get_dump_status(request.app.db, user_id)
duser['dump_status'] = dump_status
return response.json(duser)
......
......@@ -180,12 +180,13 @@ async def login_user(request):
log.info(f'login: {username!r} does not exist')
raise FailedAuth('User or password invalid')
await pwd_check(request, user['password_hash'], password)
if not user['active']:
log.warning(f'login: {username!r} is not active')
raise FailedAuth('User is deactivated')
await check_bans(request, user['user_id'])
await pwd_check(request, user['password_hash'], password)
return user
......
......@@ -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
Subproject commit 5dc606cefb6646e8d628e1d89cd1a0b7eeea04c5
Subproject commit 1aa132c38d42a8fb354e71b29615c7e41dbe73ab
......@@ -210,14 +210,16 @@ def handle_exception(request, exception):
log.warning(f'File not found: {exception!r}')
if request.app.econfig.ENABLE_FRONTEND:
# admin panel routes all 404's back to index.
# admin panel routes all 404's back to index (without a 404).
if url.startswith('/admin'):
return response.file(
'./admin-panel/build/index.html')
return response.file(
'./frontend/output/404.html',
status=404)
# if we aren't on /api/, return elixire-fe's 404 page
if not url.startswith('/api'):
return response.file(
'./frontend/output/404.html',
status=404)
else:
log.exception(f'Error in request: {exception!r}')
......
......@@ -61,6 +61,22 @@ async def test_user_fetch(test_cli):
assert isinstance(rjson['paranoid'], bool)
assert isinstance(rjson['limits'], dict)
# trying to fetch the user from the username we got
# should also work
user_id = rjson['user_id']
resp = await test_cli.get(
f'/api/admin/users/by-username/{rjson["username"]}', headers={
'Authorization': atoken
})
assert resp.status == 200
rjson = await resp.json()
# just checking the id should work, as the response of
# /by-username/ is the same as doing it by ID.
assert isinstance(rjson['user_id'], str)
assert rjson['user_id'] == user_id
async def test_user_activate_cycle(test_cli):
# logic here is to:
......@@ -153,6 +169,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):
......
......@@ -31,6 +31,13 @@ async def test_profile_work(test_cli):
# dict checking is over the test_limits_work function
assert isinstance(rjson['limits'], dict)
# test_stats already checks data
assert isinstance(rjson['stats'], dict)
dstatus = rjson['dump_status']
assert isinstance(dstatus, dict)
assert isinstance(dstatus['state'], str)
async def test_limits_work(test_cli):
utoken = await login_normal(test_cli)
......