diff --git a/dbschema/default.gel b/dbschema/default.gel index 1262fc2..0606474 100644 --- a/dbschema/default.gel +++ b/dbschema/default.gel @@ -1,6 +1,18 @@ module default { scalar type access_status extending enum; + type IPAddr { + required value: str { + constraint exclusive; + } + } + + type UserAgent { + required value: str { + constraint exclusive; + } + } + type files { required file_id: str; required file_name: str; @@ -15,7 +27,9 @@ module default { readonly := true; } - multi accesses -> file_access; + multi accesses -> file_access { + on source delete delete target if orphan; + }; required property user_id: str { readonly := true; }; @@ -24,14 +38,18 @@ module default { } type file_access { - required ip: str; + required link ip -> IPAddr { + readonly := true; + on source delete delete target if orphan; + } + required link user_agent -> UserAgent { + readonly := true; + on source delete delete target if orphan; + } + required status: access_status { default := access_status.ok; } - - user_agent: str { - readonly := true; - }; required at: datetime { readonly := true; default := datetime_of_statement(); diff --git a/my_modules/EdgeDB.py b/my_modules/EdgeDB.py index 990e2ed..4046a3c 100644 --- a/my_modules/EdgeDB.py +++ b/my_modules/EdgeDB.py @@ -1,9 +1,6 @@ from my_modules.app.logger import logger -from datetime import datetime, timedelta, timezone -from zoneinfo import ZoneInfo -from collections import Counter -import asyncio, gel, json +import asyncio, gel class EdgeDB: def __init__(self, database:str=None, tls_security:str='insecure', timeout:int=1, max_retrys:int=10): @@ -69,7 +66,7 @@ class EdgeDB: """, file_id=file_id ) - + if data: return { "file_name": data.file_name, @@ -78,7 +75,7 @@ class EdgeDB: } return None - async def get_files(self, current_datetime, user_id: str): + async def get_files(self, current_datetime, user_id:str): data = await self.run_query_with_reconnection( self.client.query, """ @@ -100,16 +97,14 @@ class EdgeDB: now=current_datetime, user_id=user_id, ) - return [ - { - "file_id": i.file_id, - "file_name": i.file_name, - "file_size": i.file_size, - "note": i.note, - "uploaded_at": i.uploaded_at, - "expires_at": i.expires_at, - } for i in data - ] + return [{ + "file_id": i.file_id, + "file_name": i.file_name, + "file_size": i.file_size, + "note": i.note, + "uploaded_at": i.uploaded_at, + "expires_at": i.expires_at if i.expires_at else '', + } for i in data] async def add_file(self, file_id, file_name, file_size, note, content_type, uploaded_at, expires_at, user_id:str): return await self.run_query_with_reconnection( @@ -180,13 +175,11 @@ class EdgeDB: """, now=current_datetime ) - return [ - { - "file_id": item.file_id, - "file_name": item.file_name, - "expires_at": item.expires_at - } for item in data - ] + return [{ + "file_id": item.file_id, + "file_name": item.file_name, + "expires_at": item.expires_at + } for item in data] async def delete_files_by_ids(self, remove_file_ids:list[str]): if not remove_file_ids: @@ -205,11 +198,105 @@ class EdgeDB: pass # File Access Quary Functions - async def add_file_access(self): - pass + async def add_file_access(self, file_id: str, ip_address: str, status: str, user_agent: str, accessed_at): + return await self.run_query_with_reconnection( + self.client.query, + """ + WITH + used_file := ( + SELECT files + FILTER .file_id = $file_id + LIMIT 1 + ), + ip_obj := ( + INSERT IPAddr { value := $ip_address } + UNLESS CONFLICT ON .value + ELSE ( + SELECT IPAddr + FILTER .value = $ip_address + ) + ), + ua_obj := ( + INSERT UserAgent { value := $user_agent } + UNLESS CONFLICT ON .value + ELSE ( + SELECT UserAgent + FILTER .value = $user_agent + ) + ), + new_file_access := ( + INSERT file_access { + at := $accessed_at, + status := $status, + ip := ip_obj, + user_agent := ua_obj + } + ), + _updated_file := ( + UPDATE used_file + SET { accesses += (SELECT new_file_access) } + ) + + SELECT new_file_access { + at, + status, + ip: { value }, + user_agent: { value }, + }; + """, + file_id=file_id, + accessed_at=accessed_at, + ip_address=ip_address, + status=status, + user_agent=str(user_agent), + ) async def get_all_file_access(self): - pass + data = await self.run_query_with_reconnection( + self.client.query, + """ + select file_access { + status, + ip: { + value + }, + user_agent: { + value + }, + at + } + """ + ) + return [{ + "status": str(file.status), + "ip": file.ip.value, + "user_agent": file.user_agent.value, + "accessed_at": file.at, + } for file in data] - async def get_file_access(self, file_id:str): - pass + async def get_file_access(self, file_id: str): + data = await self.run_query_with_reconnection( + self.client.query_single, + """ + SELECT files { + accesses: { + status, + ip: { value }, + user_agent: { value }, + at + } + } + FILTER .file_id = $file_id + LIMIT 1 + """, + file_id=file_id, + ) + + if data: + return [{ + "status": str(access.status), + "ip": access.ip.value if access.ip else None, + "user_agent": access.user_agent.value if access.user_agent else None, + "accessed_at": access.at, + } for access in data.accesses] + return None diff --git a/my_modules/file_helper_functions.py b/my_modules/file_helper_functions.py index cc4dbcc..a8fb5d2 100644 --- a/my_modules/file_helper_functions.py +++ b/my_modules/file_helper_functions.py @@ -1,7 +1,7 @@ from my_modules.app.constens import SECRET_KEY import hmac, hashlib, base64, secrets, time -from urllib.parse import quote, unquote +from datetime import datetime, timezone def base64url_encode(data: bytes) -> str: return base64.urlsafe_b64encode(data).decode().rstrip("=") @@ -27,6 +27,15 @@ def verify_signed_url(file_id: str, token: str, file_expiration: int) -> bool: not_expired = file_expiration >= time.time() return valid_sig and not_expired +def is_expired(expires_at): + if not expires_at: + return False + if expires_at.tzinfo is None: + expires_at = expires_at.replace(tzinfo=timezone.utc) + else: + expires_at = expires_at.astimezone(timezone.utc) + return expires_at <= datetime.now(timezone.utc) + if __name__ == "__main__": file_id = generate_short_id() url = generate_signed_url(file_id) diff --git a/routes/side/main.py b/routes/side/main.py index 2f7f035..45573dd 100644 --- a/routes/side/main.py +++ b/routes/side/main.py @@ -1,5 +1,6 @@ -from my_modules.file_helper_functions import verify_signed_url +from my_modules.file_helper_functions import is_expired, verify_signed_url from my_modules.decoratory.header import login_required +from my_modules.functions import get_ip from my_modules.app.setup import LIMITER from my_modules.app.logger import logger @@ -22,29 +23,20 @@ async def files(user): files_data = await current_app.edgedb.get_files(current_datetime=datetime.now(timezone.utc), user_id=user['sub']) return await render_template("views/webpage/files_list.htm", files=files_data) -@side_main_bp.route('/files//info') +@side_main_bp.route('/files//info') @LIMITER.limit("10 per minute") @login_required async def file_info(file_id, user): files_data = await current_app.edgedb.get_files(user_id=user['sub']) return await render_template("views/webpage/.htm", files=files_data) -@side_main_bp.route('/files//edit') +@side_main_bp.route('/files//edit') @LIMITER.limit("10 per minute") @login_required async def file_edit(file_id, user): files_data = await current_app.edgedb.get_files(user_id=user['sub']) return await render_template("views/webpage/.htm", files=files_data) -def is_expired(expires_at): - if not expires_at: - return False - if expires_at.tzinfo is None: - expires_at = expires_at.replace(tzinfo=timezone.utc) - else: - expires_at = expires_at.astimezone(timezone.utc) - return expires_at <= datetime.now(timezone.utc) - @side_main_bp.route("/-") @LIMITER.limit("10 per minute") async def serve_file(file_id: str): @@ -53,6 +45,7 @@ async def serve_file(file_id: str): abort(404) if is_expired(file_data.get("expires_at")): + await current_app.edgedb.add_file_access(file_id=file_id, ip_address=get_ip(), user_agent=request.user_agent, status="expired", accessed_at=datetime.now(timezone.utc)) return Response("This file has expired.", status=410, headers={ "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", @@ -65,8 +58,10 @@ async def serve_file(file_id: str): path = current_app.upload_folder / file_name if not path.exists() or not path.is_file(): + await current_app.edgedb.add_file_access(file_id=file_id, ip_address=get_ip(), user_agent=request.user_agent, status="error", accessed_at=datetime.now(timezone.utc)) abort(404) + await current_app.edgedb.add_file_access(file_id=file_id, ip_address=get_ip(), user_agent=request.user_agent, status="ok", accessed_at=datetime.now(timezone.utc)) return await send_from_directory( directory=current_app.upload_folder, file_name=file_name, diff --git a/run.py b/run.py new file mode 100755 index 0000000..0859335 --- /dev/null +++ b/run.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +import quart_flask_patch +import asyncio +asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy()) + +from my_modules.app.constens import WEB_DEBUG +import my_modules.middleware +from my_modules.app.setup import app +from routes import ( + basic_bp, auth_login_bp, + side_main_bp, + upload_bp +) + +# Views for Requests adding the uris +app.register_blueprint(basic_bp) +app.register_blueprint(auth_login_bp, url_prefix='/auth') + +app.register_blueprint(side_main_bp) +app.register_blueprint(upload_bp) + +if __name__ == '__main__': + app.run(debug=WEB_DEBUG, port=5500) diff --git a/templates/side/views/webpage/files_list.htm b/templates/side/views/webpage/files_list.htm index 23c82c0..accdd0f 100644 --- a/templates/side/views/webpage/files_list.htm +++ b/templates/side/views/webpage/files_list.htm @@ -227,13 +227,17 @@ {{ file.note }} {{ file.file_size }} - - + +
- - - + + +
@@ -245,7 +249,6 @@ {% endblock %}