diff --git a/Telegram_provider/main.py b/Telegram_provider/main.py index d998cfe..a91aca7 100644 --- a/Telegram_provider/main.py +++ b/Telegram_provider/main.py @@ -14,7 +14,7 @@ import time import asyncio import io -import aiohttp +import fileuploader """Создание всех нужных объектов""" load_dotenv() @@ -30,25 +30,6 @@ class States(StatesGroup): login_register = State() -async def upload(bytes: bytearray, filename: str, user): - form_data = aiohttp.FormData() - form_data.add_field('file', bytes, filename=filename) - headers = {} - if user: - headers = {"Authorization": "Bearer " + user.token} - - async with aiohttp.ClientSession("https://fu.andcool.ru") as session: - async with session.post(f"/api/upload/private", - data=form_data, headers=headers) as r: - return r, await r.json() - - -async def get_files(token: str): - async with aiohttp.ClientSession("https://fu.andcool.ru") as session: - async with session.get(f"/api/get_files/private", headers={"Authorization": "Bearer " + token}) as r: - return r, await r.json() - - async def send_login_message(message: types.Message): builder = InlineKeyboardBuilder() builder.add(types.InlineKeyboardButton( @@ -85,26 +66,24 @@ async def account(message: types.Message, state: FSMContext): return else: - response, response_json = await get_files(user.token) - if response.status == 401: + try: + user_obj = await fileuploader.User.loginToken(user.token) + builder = InlineKeyboardBuilder() + builder.add(types.InlineKeyboardButton( + text="Log out", + callback_data=f"logout") + ) + + await message.answer(f"*Account:*\n*Username:* {user_obj.username}\n", + reply_markup=builder.as_markup(), + parse_mode="Markdown") + except fileuploader.exceptions.NotAuthorized: await db.user.delete(where={"id": user.id}) await send_login_message(message) return - - if response.status == 429: + except fileuploader.exceptions.TooManyRequests: await message.answer("Sorry, the servers are overloaded right now") return - - builder = InlineKeyboardBuilder() - builder.add(types.InlineKeyboardButton( - text="Log out", - callback_data=f"logout") - ) - - await message.answer(f"*Account:*\n*Username:* {user.username}\n" + \ - f"*Files on account:* {len(response_json['data'])}", - reply_markup=builder.as_markup(), - parse_mode="Markdown") @dp.callback_query(F.data.startswith("log_")) @@ -125,45 +104,49 @@ async def log_reg(message: types.Message, state: FSMContext): await message.answer('The data was sent incorrectly') return - async with aiohttp.ClientSession("https://fu.andcool.ru") as session: - async with session.post(f"/api/{'login' if login_register == 'login' else 'register'}?bot=true", - json={"username": login_and_password[0], - "password": login_and_password[1]}) as r: - - await message.delete() - data_resp = await r.json() - if r.status == 400 or r.status == 404: - await message.answer(data_resp['message']) - return - - if r.status == 200: - await db.user.create(data={ + try: + if login_register == 'login': + user = await fileuploader.User.login(login_and_password[0], login_and_password[1]) + else: + user = await fileuploader.User.register(login_and_password[0], login_and_password[1]) + + await db.user.create(data={ "user_id": message.from_user.id, - "username": data_resp['username'], - 'token': data_resp['accessToken'] - }) - await account(message, state) - await state.clear() - return - - await message.answer("Unhandled error") - return + "username": user.username, + 'token': user.accessToken + }) + await account(message, state) + await state.clear() + return + except fileuploader.exceptions.UserAreadyRegistered: + await message.answer("User with this username was already registered") + return + except fileuploader.exceptions.WrongPassword: + await message.answer("Wrong password") + return + except fileuploader.exceptions.UserNotFound: + await message.answer("User not found") + return + finally: + await message.delete() @dp.callback_query(F.data == "logout") async def log(callback: types.CallbackQuery, state: FSMContext): - user = await db.user.find_first(where={"user_id": callback.from_user.id}) - if not user: + user_db = await db.user.find_first(where={"user_id": callback.from_user.id}) + if not user_db: await callback.message.answer("You are not logged in") return - async with aiohttp.ClientSession("https://fu.andcool.ru") as session: - async with session.get(f"/api/logout", headers={"Authorization": "Bearer " + user.token}) as response: - if response.status == 401 or response.status == 200: - await db.user.delete(where={"id": user.id}) - await callback.message.answer("Logged out!") - await callback.message.delete() - return + user = fileuploader.User.User() + user.accessToken = user_db.token + try: + await user.logout() + finally: + await db.user.delete(where={"id": user_db.id}) + await callback.message.answer("Logged out!") + await callback.message.delete() + return @dp.message(F.content_type.in_({'document', 'photo', 'video', 'animation', 'video_note', 'voice', 'text'})) @@ -235,22 +218,32 @@ async def send_file(message: types.Message, state: FSMContext): filename = 'text.txt' user = await db.user.find_first(where={"user_id": message.from_user.id}) - response, result = await upload(file_bytes, filename, user) - - if response.status != 200: - error = result['error'] if response.status == 429 else result['message'] - await message.reply(error) - return + user_obj = fileuploader.User.User(user.token) - builder = InlineKeyboardBuilder() - builder.add(types.InlineKeyboardButton( - text="Delete file", - callback_data=f"delete_{result['file_url']}_{result['key']}") - ) + try: + file = await fileuploader.upload(file_bytes, filename, user=user_obj) + builder = InlineKeyboardBuilder() + builder.add(types.InlineKeyboardButton( + text="Delete file", + callback_data=f"delete_{file.file_url}_{file.key}") + ) - await message.reply(f"*Your file has been uploaded.!*\n*Link:* {result['file_url_full']}\n*Size:* {result['size']}", - reply_markup=builder.as_markup(), - parse_mode="Markdown") + await message.reply(f"*Your file has been uploaded.!*\n*Link:* {file.file_url_full}\n*Size:* {file.size}", + reply_markup=builder.as_markup(), + parse_mode="Markdown") + + except fileuploader.exceptions.FileSizeExceedsTheLimit: + await message.reply("File size exceeds the limit (100MB)") + return + except fileuploader.exceptions.InvalidGroup: + await message.reply("Invalid group") + return + except fileuploader.exceptions.GroupNotFound: + await message.reply("Group not found") + return + except fileuploader.exceptions.YouAreNotInTheGroup: + await message.reply("You are not in the group") + return @dp.callback_query(F.data.startswith("delete_")) @@ -258,15 +251,15 @@ async def delete_file(callback: types.CallbackQuery, state: FSMContext): """Хэндлер для колбэка кнопок выбора удаления""" file_data = callback.data.replace("delete_", "").split("_") - async with aiohttp.ClientSession("https://fu.andcool.ru") as session: - async with session.get(f"/api/delete/{file_data[0]}?key={file_data[1]}") as r: - response = await r.json() - if response['status'] == 'success': - await callback.message.delete() - await callback.message.answer("File has been deleted!") - else: - await callback.message.answer(response['message']) - + try: + await fileuploader.delete(file_data[0], file_data[1]) + await callback.message.delete() + await callback.message.answer("File has been deleted!") + + except fileuploader.exceptions.FileNotFound: + await callback.message.answer("File not found") + except fileuploader.exceptions.InvalidUniqueKey: + await callback.message.answer("Error while deleting file") async def start(): """Асинхронная функция для запуска диспатчера""" diff --git a/accept_invite.html b/accept_invite.html index 77c0621..f31ef97 100644 --- a/accept_invite.html +++ b/accept_invite.html @@ -115,6 +115,7 @@

Loading...

const host = window.location.host; window.location.replace(protocol + "//" + host + page_url); } + function parseJwt (token) { var base64Url = token.split('.')[1]; var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); @@ -132,6 +133,17 @@

Loading...

return (parseInt(res["ExpiresAt"]) - 259200) > secondsSinceEpoch; } + async function get_new_tokens(accessToken) { + try { + let response = await axios.post(api_url + "/api/refresh_token", { 'accessToken': "Bearer " + accessToken }, {}) + if (!response) return false; + if (response.status != 200) return false; + return response.data.accessToken; + } catch { + return false; + } + } + async function fetch_info(invite_link) { let accessToken = localStorage.getItem("accessToken"); if (!accessToken) moveToPage("/login"); diff --git a/imports.py b/imports.py index fc02171..b69d962 100644 --- a/imports.py +++ b/imports.py @@ -19,5 +19,6 @@ import jwt import bcrypt import random +import json rate_limit_exceeded_handler = _rate_limit_exceeded_handler diff --git a/main.py b/main.py index 7dc0db2..7b7dc5b 100644 --- a/main.py +++ b/main.py @@ -58,7 +58,7 @@ async def api(request: Request): ) -async def check_token(Authorization): +async def check_token(Authorization, user_agent): if not Authorization: # If token doesn't provided return None, {"message": "No Authorization header provided", "errorId": -1} @@ -86,6 +86,10 @@ async def check_token(Authorization): if token["ExpiresAt"] < time.time(): # If token expired await db.token.delete(where={"id": token_db.id}) return None, {"message": "Access token expired", "errorId": -3} + + if user_agent != token_db.fingerprint and token_db.fingerprint != "None": + await db.token.delete(where={"id": token_db.id}) + return None, {"message": "Invalid fingerprint", "errorId": -6} return token_db, {} @@ -104,9 +108,8 @@ async def upload_file( request: Request, include_ext: bool = False, max_uses: int = 0, - Authorization: Annotated[ - Union[str, None], Header(convert_underscores=False) - ] = None, + Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None, + user_agent: Union[str, None] = Header(default=None) ): if not file: # Check, if the file is uploaded @@ -132,7 +135,7 @@ async def upload_file( saved_to_account = False user_id = -1 - token_db, auth_error = await check_token(Authorization) # Check token + token_db, auth_error = await check_token(Authorization, user_agent) # Check token if token_db: # If token is okay saved_to_account = True user_id = token_db.user.id @@ -303,11 +306,10 @@ async def delete_file(url: str, key: str = ""): async def getFiles( group_id: str, request: Request, - Authorization: Annotated[ - Union[str, None], Header(convert_underscores=False) - ] = None, + Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None, + user_agent: Union[str, None] = Header(default=None) ): - token_db, auth_error = await check_token(Authorization) # Check token validity + token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity if not token_db: # If token is not valid return JSONResponse( content={ @@ -385,11 +387,19 @@ async def getFiles( @app.post("/api/register") # Registartion handler @limiter.limit(dynamic_limit_provider) -async def register(request: Request, bot: bool = False): - body = await request.json() - if ( - "username" not in body or "password" not in body - ): # If request body doesn't have username and password field +async def register(request: Request, bot: bool = False, user_agent: Union[str, None] = Header(default=None)): + try: + body = await request.json() + except json.decoder.JSONDecodeError: + return JSONResponse( + { + "status": "error", + "message": "No username/password provided", + "errorId": 2, + }, + status_code=400, + ) + if ("username" not in body or "password" not in body): # If request body doesn't have username and password field return JSONResponse( { "status": "error", @@ -431,6 +441,7 @@ async def register(request: Request, bot: bool = False): await db.token.create( { # Create token record in db "accessToken": access, + "fingerprint": user_agent, "user": { "connect": { "id": user.id, @@ -449,10 +460,128 @@ async def register(request: Request, bot: bool = False): ) +@app.get("/api/login/token") # login by token handler +@limiter.limit(dynamic_limit_provider) +async def login_token(request: Request, + Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None, + user_agent: Union[str, None] = Header(default=None)): + token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity + if not token_db: # If token is not valid + return JSONResponse( + content={ + "status": "error", + "message": "Auth error", + "auth_error": auth_error, + }, + status_code=401, + ) + return JSONResponse({"status": "success", + "message": "Logged in by token", + "username": token_db.user.username, + "accessToken": token_db.accessToken}) + + +@app.post("/api/login/discord/{code}") # login handler +@limiter.limit(dynamic_limit_provider) +async def login(code: str, + request: Request, + user_agent: Union[str, None] = Header(default=None)): + + data = { + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': "https://fu.andcool.ru/login/discord" + } + headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + + auth = aiohttp.BasicAuth(login=os.getenv("DISCORD_CLIENT_ID"), password=os.getenv("DISCORD_CLIENT_SECRET")) + + response_json = {} + async with aiohttp.ClientSession() as session: + async with session.post(f'https://discord.com/api/v10/oauth2/token', data=data, headers=headers, auth=auth) as response: + response_json = await response.json() + if response.status != 200: + return JSONResponse({"status": "error", "message": "Internal error, please, log in again"}, status_code=401) + + async with session.get('https://discord.com/api/users/@me', + headers={"Authorization": f"{response_json['token_type']} {response_json['access_token']}"}) as response_second: + if response_second.status != 200: + return JSONResponse({"status": "error", "message": "Invalid token, please, log in again"}, status_code=401) + response_user_json = await response_second.json() + user_check = await db.user.find_first(where={"discord_uid": response_user_json["id"]}) + + if not user_check: + user = await db.user.create( # Create user record in db + {"username": str(response_user_json["global_name"]), "password": "None", "discord_uid": response_user_json["id"]} + ) + access = jwt.encode( + { + "user_id": int(user.id), + "ExpiresAt": time.time() + accesLifeTime, + }, + "accessTokenSecret", + algorithm="HS256", + ) # Generate token + + await db.token.create( + { # Create token record in db + "accessToken": access, + "fingerprint": user_agent, + "user": { + "connect": { + "id": user.id, + }, + }, + } + ) + else: + access = jwt.encode( + { + "user_id": int(user_check.id), + "ExpiresAt": time.time() + accesLifeTime, + }, + "accessTokenSecret", + algorithm="HS256", + ) # Generate token + + await db.token.create( + { # Create token record in db + "accessToken": access, + "fingerprint": user_agent, + "user": { + "connect": { + "id": user_check.id, + }, + }, + } + ) + + return JSONResponse( + { + "status": "success", + "accessToken": access, + "username": response_user_json["global_name"], + "message": "registred" if not user_check else "logged in", + }, + status_code=200, + ) + @app.post("/api/login") # login handler @limiter.limit(dynamic_limit_provider) -async def login(request: Request, bot: bool = False): - body = await request.json() +async def login(request: Request, bot: bool = False, user_agent: Union[str, None] = Header(default=None)): + try: + body = await request.json() + except json.decoder.JSONDecodeError: + return JSONResponse( + { + "status": "error", + "message": "No username/password provided", + "errorId": 2, + }, + status_code=400, + ) if ("username" not in body or "password" not in body): # If request body doesn't have username and password field return JSONResponse( { @@ -474,7 +603,11 @@ async def login(request: Request, bot: bool = False): ) user = await db.user.find_first( - where={"username": body["username"]}, include={"tokens": True} + where={'AND': [ + {"username": body["username"]}, + {'NOT':[{"password": "None"}]} + ] + }, include={"tokens": True} ) # Find same username in db if not user: # If user doesn't exists return JSONResponse( @@ -482,9 +615,7 @@ async def login(request: Request, bot: bool = False): status_code=404, ) - if bcrypt.checkpw( - bytes(body["password"], "utf-8"), bytes(user.password, "utf-8") - ): # If password is correct + if bcrypt.checkpw(bytes(body["password"], "utf-8"), bytes(user.password, "utf-8")): # If password is correct access = jwt.encode( { "user_id": int(user.id), @@ -500,6 +631,7 @@ async def login(request: Request, bot: bool = False): await db.token.create( { # Create token record in db "accessToken": access, + "fingerprint": user_agent, "user": { "connect": { "id": user.id, @@ -523,7 +655,7 @@ async def login(request: Request, bot: bool = False): @app.post("/api/refresh_token") # refresh token handler @limiter.limit(dynamic_limit_provider) -async def refresh_token(request: Request): +async def refresh_token(request: Request, user_agent: Union[str, None] = Header(default=None)): body = await request.json() if "accessToken" not in body: # If token doesn't provided return JSONResponse( @@ -531,9 +663,7 @@ async def refresh_token(request: Request): status_code=400, ) - token_db, auth_error = await check_token( - body["accessToken"] - ) # Check token validity + token_db, auth_error = await check_token(body["accessToken"], user_agent) # Check token validity if not token_db: # If token is not valid return JSONResponse( content={ @@ -560,12 +690,11 @@ async def refresh_token(request: Request): @limiter.limit(dynamic_limit_provider) async def logout( request: Request, - Authorization: Annotated[ - Union[str, None], Header(convert_underscores=False) - ] = None, + Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None, + user_agent: Union[str, None] = Header(default=None) ): - token_db, auth_error = await check_token(Authorization) # Check token validity + token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity if not token_db: # If token is not valid return JSONResponse( content={ @@ -585,12 +714,11 @@ async def logout( @limiter.limit(dynamic_limit_provider) async def transfer( request: Request, - Authorization: Annotated[ - Union[str, None], Header(convert_underscores=False) - ] = None, + Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None, + user_agent: Union[str, None] = Header(default=None) ): - token_db, auth_error = await check_token(Authorization) # Check token validity + token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity if not token_db: # If token is not valid return JSONResponse( content={ @@ -639,9 +767,8 @@ async def transfer( @limiter.limit(dynamic_limit_provider) async def create_group( request: Request, - Authorization: Annotated[ - Union[str, None], Header(convert_underscores=False) - ] = None, + Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None, + user_agent: Union[str, None] = Header(default=None) ): body = await request.json() @@ -650,7 +777,7 @@ async def create_group( {"status": "error", "message": "No `group_name` provided"}, status_code=400 ) - token_db, auth_error = await check_token(Authorization) # Check token validity + token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity if not token_db: # If token is not valid return JSONResponse( content={ @@ -692,12 +819,11 @@ async def create_group( async def delete_group( group_id: int, request: Request, - Authorization: Annotated[ - Union[str, None], Header(convert_underscores=False) - ] = None, + Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None, + user_agent: Union[str, None] = Header(default=None) ): - token_db, auth_error = await check_token(Authorization) # Check token validity + token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity if not token_db: # If token is not valid return JSONResponse( content={ @@ -733,12 +859,11 @@ async def delete_group( async def generate_invite( group_id: int, request: Request, - Authorization: Annotated[ - Union[str, None], Header(convert_underscores=False) - ] = None, + Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None, + user_agent: Union[str, None] = Header(default=None) ): - token_db, auth_error = await check_token(Authorization) # Check token validity + token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity if not token_db: # If token is not valid return JSONResponse( content={ @@ -780,12 +905,11 @@ async def generate_invite( async def delete_group( invite_link: str, request: Request, - Authorization: Annotated[ - Union[str, None], Header(convert_underscores=False) - ] = None, + Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None, + user_agent: Union[str, None] = Header(default=None) ): - token_db, auth_error = await check_token(Authorization) # Check token validity + token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity if not token_db: # If token is not valid return JSONResponse( content={ @@ -837,12 +961,11 @@ async def delete_group( async def delete_group( invite_link: str, request: Request, - Authorization: Annotated[ - Union[str, None], Header(convert_underscores=False) - ] = None, + Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None, + user_agent: Union[str, None] = Header(default=None) ): - token_db, auth_error = await check_token(Authorization) # Check token validity + token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity if not token_db: # If token is not valid return JSONResponse( content={ @@ -874,12 +997,11 @@ async def delete_group( async def delete_group( group_id: int, request: Request, - Authorization: Annotated[ - Union[str, None], Header(convert_underscores=False) - ] = None, + Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None, + user_agent: Union[str, None] = Header(default=None) ): - token_db, auth_error = await check_token(Authorization) # Check token validity + token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity if not token_db: # If token is not valid return JSONResponse( content={ @@ -915,12 +1037,11 @@ async def delete_group( @limiter.limit(dynamic_limit_provider) async def get_groups( request: Request, - Authorization: Annotated[ - Union[str, None], Header(convert_underscores=False) - ] = None, + Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None, + user_agent: Union[str, None] = Header(default=None) ): - token_db, auth_error = await check_token(Authorization) # Check token validity + token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity if not token_db: # If token is not valid return JSONResponse( content={ diff --git a/schema.prisma b/schema.prisma index 68477e8..079a3a6 100644 --- a/schema.prisma +++ b/schema.prisma @@ -29,16 +29,18 @@ model file { } model User { - id Int @id @default(autoincrement()) - username String @default("") - password String @default("") - tokens Token[] - groups Group[] + id Int @id @default(autoincrement()) + username String @default("") + password String @default("") + discord_uid BigInt @default(-1) + tokens Token[] + groups Group[] } model Token{ id Int @id @default(autoincrement()) accessToken String @default("") + fingerprint String @default("None") user User @relation(fields: [user_id], references: [id]) user_id Int @default(0) } diff --git a/web/login/discord/index.html b/web/login/discord/index.html new file mode 100644 index 0000000..3239880 --- /dev/null +++ b/web/login/discord/index.html @@ -0,0 +1,141 @@ + + + + + File uploader + + + + + + + +
+

File uploader

+ +
+
+

Loading...

+
+
+ + + + \ No newline at end of file diff --git a/web/login/index.html b/web/login/index.html index e7fc368..80a11d5 100644 --- a/web/login/index.html +++ b/web/login/index.html @@ -42,6 +42,7 @@

Login

+ Login by Discord diff --git a/web/login/style.css b/web/login/style.css index ffb31b3..2207a19 100644 --- a/web/login/style.css +++ b/web/login/style.css @@ -86,6 +86,17 @@ nav a:hover{ text-decoration: underline dashed; } .selected{font-weight: 600;} + +.discord{ + cursor: pointer; + color: white; + text-decoration: none; + +} +.discord:hover{ + text-decoration: underline dashed; +} + @media(max-width: 425px){ main{width: 90%;} h1{font-size: 130%;} diff --git a/web/res/ds.svg b/web/res/ds.svg new file mode 100644 index 0000000..f905d23 --- /dev/null +++ b/web/res/ds.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/uploaders/File uploader.sxcu b/web/uploaders/File uploader.sxcu index d030048..ab2b543 100644 --- a/web/uploaders/File uploader.sxcu +++ b/web/uploaders/File uploader.sxcu @@ -3,7 +3,7 @@ "Name": "File uploader", "DestinationType": "ImageUploader, FileUploader", "RequestMethod": "POST", - "RequestURL": "https://fu.andcool.ru/api/upload", + "RequestURL": "https://fu.andcool.ru/api/upload/private", "Parameters": { "include_ext": "true" }, diff --git a/web/uploaders/index.html b/web/uploaders/index.html index b2f82b0..8a873bc 100644 --- a/web/uploaders/index.html +++ b/web/uploaders/index.html @@ -23,9 +23,18 @@

File uploader


by AndcoolSystems

+

Bots

+

Discord

+

Telegram

+

ShareX

Click here to download the ShareX config

- + +

Python package

+

Python package for working with RESTful API

+ +

API Docs

+

API documentation

\ No newline at end of file