From 93cc621778cf5bf1a0e828c3a3a7bae92e79efb1 Mon Sep 17 00:00:00 2001 From: AndcoolSystems Date: Wed, 31 Jan 2024 16:32:38 +0300 Subject: [PATCH] Added bot query parameter in login/register endpoint --- .gitignore | 3 +- README.md | 140 ++++++++++++++++++++++++++++---------------------- config.py | 3 +- main.py | 68 +++++++++++++++--------- schema.prisma | 1 + web/script.js | 4 +- 6 files changed, 130 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index ba91b55..aad3bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /uploads /__pycache__ /dataBase.db -/.env \ No newline at end of file +/.env +/tg \ No newline at end of file diff --git a/README.md b/README.md index fae6c96..a221a6b 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,31 @@ # File Uploader -The file uploader is implemented in the Python programming language using FastAPI. All uploaded files are stored on the server. The API provides the ability to preview and download files based on their file extensions.
+The file uploader is implemented in the Python programming language using FastAPI. All uploaded files are stored on the server. The API provides the ability to preview and download files based on their file extensions. ## API Documentation -The API is currently hosted at [fu.andcool.ru](https://fu.andcool.ru/).
-Page redirection is handled through the nginx proxy server. The API consists of 3 endpoint URLs:
+The API is currently hosted at [fu.andcool.ru](https://fu.andcool.ru/). +Page redirection is handled through the nginx proxy server. The API consists of 8 endpoint URLs: -- `/file/` — Endpoint where all files are located. -- `/api/upload` — Endpoint for receiving file upload requests. -- `/api/delete/` — Request to delete a file. +- `/file/` – Endpoint where all files are located. +- `/api` – Main API endpoint +- - `/upload` – Endpoint for receiving file upload requests. +- - `/delete/` – Request to delete a file. +- - `/login` – Log in with login and password. +- - `/register` – Create a new account with a login and password. +- - `/refresh_token` – Refresh the existing token. +- - `/get_files` (`/getFiles` deprecated) – Get list of files +- - `/logout` – Log out from account ### 1.1 Authorization Errors -All requests requiring the `Authorization` header may encounter errors related to authorization issues.
-The `Authorization` header should have the format `Authorization: Bearer `.
+All requests requiring the `Authorization` header may encounter errors related to authorization issues. +The `Authorization` header should have the format `Authorization: Bearer `. #### Response Example -All errors of this type follow a consistent response format and always return an HTTP code of `401`.
-This section will be referred to as `1.1` in the documentation.
+All errors of this type follow a consistent response format and always return an HTTP code of `401`. +This section will be referred to as `1.1` in the documentation. ```json { @@ -31,7 +37,7 @@ This section will be referred to as `1.1` in the documentation.
} ``` -List of errors:
+**List of errors:** | errorId | message | Reasons | | ------- | ----------------------------------------------------- | --------------------------------------------- | @@ -43,9 +49,10 @@ List of errors:
### 1.2 Basic API -`GET /file/` — Retrieves a file based on the URL.
-Successful execution returns a `200` status code and the binary file with the `Content-Type`.
-If the file type cannot be determined, the API returns the file in download mode.
+### Retrieve a file based on the URL. +`GET /file/` or `GET /f/` +Successful execution returns a `200` status code and the binary file with the `Content-Type`. +If the file type cannot be determined, the API returns the file in download mode. #### Possible Errors @@ -53,18 +60,23 @@ If the file type cannot be determined, the API returns the file in download mode | ---------- | ----------------------------- | ------------------------------------------ | | 404 | File not found | The file referenced by the code does not exist | -`POST /api/upload?include_ext=false` — Uploads a file to the server.
-The request body should contain the file to be uploaded.
-Only one file is allowed, and its size should not exceed 100MB.
-The query parameter `include_ext` can be set to `true/false` to indicate whether the file extension should be included in the file URL.
-The maximum request frequency is **2 per minute**.
-The request can also include the `Authorization` header, containing the user's unique token.
+### Upload a file to the server +`POST /api/upload?include_ext=false` +The request body should contain the file to be uploaded. +Only one file is allowed, and its size should not exceed 100MB. +The maximum request frequency is **2 per minute**. -If the token is not provided or is not valid, the `synced` field in the response body will be set to `false`. The file will be uploaded to the server regardless of whether the `Authorization` header is included in the request. The `auth_error` field in the response body contains the authentication error (section `1.1`), and if there is no error, the field will be `{}`.
+**Request body:** +> **The `Content-Type` header of the request must be a `multipart/form-data`** +The file must be have `file` key in request body. +The query parameter `include_ext` can be set to `true/false` to indicate whether the file extension should be included in the file URL. -#### Response Example +**Request headers:** +>The request can also include the `Authorization` header, containing the user's unique token. +If the token is not provided or is not valid, the `synced` field in the response body will be set to `false`. The file will be uploaded to the server regardless of whether the `Authorization` header is included in the request. The `auth_error` field in the response body contains the authentication error (section `1.1`), and if there is no error, the field will be `{}`. -Upon successful execution, the API returns a `200` status code along with a JSON response.
+#### Response Example +On successful execution, the API returns a `200` HTTP code along with a JSON response. ```json { @@ -81,15 +93,15 @@ Upon successful execution, the API returns a `200` status code along with a JSON ``` #### Possible Errors - -| Error Code | Description | Possible Reasons | +| Error Code | Description | Possible Reasons | | ---------- | ------------------------------ | ---------------------------------------- | -| 400 | No file uploaded | No file is present in the request body | +| 400 | No file uploaded | No file is given in the request body | | 400 | Bad file extension | The file does not have an extension | | 413 | File size exceeds the limit (100MB) | The file size exceeds 100MB | -`GET /api/delete/?key=` — Deletes a file.
-Successful execution returns a `200` status code, removing the file from the server.
+### Delete a file +`GET /api/delete/?key=` +Successful execution returns a `200` status code, removing the file from the server. #### Possible Errors @@ -99,9 +111,12 @@ Successful execution returns a `200` status code, removing the file from the ser | 400 | Invalid unique key | The provided unique key is invalid | ### 1.2 Authorization API - -`POST /api/register or login` — Registers a new account / logs into an account.
-Request limit per minute: 10 times. Both requests accept the same request body but have different errors.
+### Login and register +`POST /api/register` +`POST /api/login` +Request limit per minute: 10 times. +Both requests accept the same request body but have different errors. +> A Boolean value can be passed to the optional query parameter `bot`. When `bot` is true, a token with a lifetime of 6 months will be generated. #### Request Example @@ -112,7 +127,7 @@ Request limit per minute: 10 times. Both requests accept the same request body b } ``` -Successful execution returns a `200` status code, indicating successful registration / login.
+Successful execution returns a `200` HTTP code, indicating successful registration / login. ```json { @@ -123,53 +138,56 @@ Successful execution returns a `200` status code, indicating successful registra } ``` -#### Possible Errors +### Possible Errors **Common for both requests:** -| errorId | message | Reasons | -| ------- | --------------------------------------------- | ------------------------------------------------ | -| 2 | No username/password provided | Username/password fields are missing in the request | +| errorId | HTTP code |message | Reasons | +| ------- | ----------|---------------------------------| --------------------------------------------------- | +| 2 | 400 | No username/password provided | Username/password fields are missing in the request | **Errors for /register:** -| errorId | message | Reasons | -| ------- | --------------------------------------------- | ------------------------------------------------ | -| 1 | An account with this name is already registered | A user with the given username already exists | +| errorId | HTTP code | message | Reasons | +| ------- | ----------|------------------------------------------------| ----------------------------------------------| +| 1 | 400 |An account with this name is already registered | A user with the given username already exists | **Errors for /login:** -| errorId | message | Reasons | -| ------- | --------------------------------------------- | ------------------------------------------------ | -| 3 | Wrong password | Incorrect password | -| 4 | User not found | Username not found | +| errorId | HTTP code | message | Reasons | +| ------- | ----------|----------------------| -----------------------| +| 3 | 400 |Wrong password | Incorrect password | +| 4 | 404 |User not found | Username not found | -`POST /api/refresh_token` — Refreshes the token.
-Request limit per minute: 10 times.
-The request body includes the `accessToken` field containing only the token (without `Bearer`).
-Successful execution returns a `200` status code along with the `accessToken` field in the request body, containing the new token.
+### Refreshe the token +`POST /api/refresh_token` +Request limit per minute: 10 times. +The request body includes the `accessToken` field containing only the token (without `Bearer`). +Successful execution returns a `200` HTTP code along with the `accessToken` field in the request body, containing the new token. #### Possible Errors -| errorId | message | Reasons | -| ------- | --------------------------------------------- | ------------------------------------------------ | -| 5 | No access token provided | The `accessToken` field is missing in the request | - -Errors described in section `1.1` may also occur.
+| errorId | HTTP code | message | Reasons | +| ------- | ----------|-----------------------------| ------------------------------------------------ | +| 5 | 400 | No access token provided | The `accessToken` field is missing in the request | -`POST /api/logout` — Logs out of the account.
-Request limit per minute: 10 times.
-It takes the `Authorization` header containing the access token.
-Successful execution of the request deletes the provided token and returns a `200` status code
+Errors described in section `1.1` may also occur. +### Log out of the account +`POST /api/logout` +Request limit per minute: 10 times. +Endpoint takes the `Authorization` header containing the access token. +Successful execution of the request deletes the provided token and returns a `200` HTTP code #### Possible Errors -Errors described in section `1.1` may occur as well.
+Errors described in section `1.1` may occur as well. + -`GET /api/getFiles` — Gets a list of files.
-It takes the `Authorization` header containing the access token.
-Retrieves a list of all files associated with this account.
+### Get a list of files. +`GET /api/get_files` +It takes the `Authorization` header containing the access token. +Retrieves a list of all files associated with this account. #### Response Example @@ -203,4 +221,4 @@ Retrieves a list of all files associated with this account.
#### Possible Errors -Errors described in section `1.1` may occur as well.
\ No newline at end of file +Errors described in section `1.1` may occur as well. \ No newline at end of file diff --git a/config.py b/config.py index 966e74c..889e276 100644 --- a/config.py +++ b/config.py @@ -29,4 +29,5 @@ } default = "application/x-msdownload" -accesLifeTime = 432_000 \ No newline at end of file +accesLifeTime = 432_000 +accesLifeTimeBot = 15_552_000 \ No newline at end of file diff --git a/main.py b/main.py index 30cc989..3420c3f 100644 --- a/main.py +++ b/main.py @@ -23,7 +23,25 @@ import jwt import bcrypt -limiter = Limiter(key_func=get_remote_address) +def custom_key_func(request: Request): + if get_remote_address(request) == os.getenv('SERVER_IP'): + return "bots" + return "user" + + +def dynamic_limit_provider(key: str): + if key == "bots": + return "1000/minute" + return "10/minute" + + +def dynamic_limit_provider_upload(key: str): + if key == "bots": + return "500/minute" + return "2/minute" + + +limiter = Limiter(key_func=custom_key_func) app = FastAPI() db = Prisma() load_dotenv() @@ -38,21 +56,17 @@ allow_headers=["*"], ) -file_life_time = 2_592_000 # File life time (unused) -check_period = 86_400 # File check period (unused) - - - - @app.on_event("startup") async def startup_event(): await db.connect() # Connecting to database print("Connected to Data Base") -"""@app.get("/favicon.ico") # Favicon handler -def get_favicon(): - return FileResponse(path="1.png") """ +@app.get("/api") # Get file handler +@limiter.limit(dynamic_limit_provider) +async def api(request: Request): + return JSONResponse(content={"status": "success", "message": "File uploader RESTful API", + "docs": "https://github.com/Andcool-Systems/File-uploader/blob/main/README.md"}, status_code=200) async def check_token(Authorization): @@ -68,18 +82,20 @@ async def check_token(Authorization): except jwt.exceptions.DecodeError: return None, {"message": "Invalid access token", "errorId": -4} - if token["ExpiresAt"] < time.time(): # If token expired - return None, {"message": "Access token expired", "errorId": -3} - token_db = await db.token.find_first(where={"accessToken": token_header[1]}) # Find token in db + if not token_db: # If not found return None, {"message": "Token not found", "errorId": -5} + 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} + return token_db, {} @app.post("/api/upload") # File upload handler -@limiter.limit(f"2/minute") +@limiter.limit(dynamic_limit_provider_upload) async def upload_file(file: UploadFile, request: Request, include_ext: bool = False, max_uses: int = 0, Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None): if not file: # Check, if the file is uploaded @@ -121,6 +137,7 @@ async def upload_file(file: UploadFile, request: Request, include_ext: bool = Fa "key": key, "type": filetypes.get(ext[1:], default) if ext.lower()[1:] in filetypes else "download", "ext": ext, + "size": file.size, "user_filename": file.filename, "max_uses": max_uses }) @@ -132,6 +149,7 @@ async def upload_file(file: UploadFile, request: Request, include_ext: bool = Fa "file_url_full": "https://fu.andcool.ru/file/" + created.url, "key": created.key, "ext": created.ext, + "size": utils.calculate_size(file.size), "user_filename": user_filename, "craeted_at": created.craeted_at, "synced": saved_to_account, @@ -140,7 +158,7 @@ async def upload_file(file: UploadFile, request: Request, include_ext: bool = Fa @app.get("/file/{url}") # Get file handler @app.get("/f/{url}") -@limiter.limit(f"10/minute") +@limiter.limit(dynamic_limit_provider) async def send_file(url: str, request: Request): result = await db.file.find_first(where={"url": url}) # Get file by url if not result: @@ -188,7 +206,8 @@ async def delete_file(url: str, key: str = ""): @app.get("/api/getFiles") # get files handler -@limiter.limit(f"10/minute") +@app.get("/api/get_files") # get files handler +@limiter.limit(dynamic_limit_provider) async def getFiles(request: Request, Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None): token_db, auth_error = await check_token(Authorization) # Check token validity @@ -207,14 +226,15 @@ async def getFiles(request: Request, "user_filename": file.user_filename, "creation_date": file.created_date, "craeted_at": file.craeted_at, + "size": utils.calculate_size(file.size), "synced": True }) return JSONResponse(content={"status": "success", "message": "messages got successfully", "username": user.username, "data": files_response}, status_code=200) @app.post("/api/register") # Registartion handler -@limiter.limit(f"10/minute") -async def register(request: Request): +@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 @@ -234,7 +254,7 @@ async def register(request: Request): "password": str(hashed.decode('utf-8')) } ) - access = jwt.encode({"user_id": int(user.id), "ExpiresAt": time.time() + accesLifeTime}, + access = jwt.encode({"user_id": int(user.id), "ExpiresAt": time.time() + (accesLifeTime if not bot else accesLifeTimeBot)}, "accessTokenSecret", algorithm="HS256") # Generate token await db.token.create({ # Create token record in db @@ -249,8 +269,8 @@ async def register(request: Request): @app.post("/api/login") # login handler -@limiter.limit(f"10/minute") -async def login(request: Request): +@limiter.limit(dynamic_limit_provider) +async def login(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 @@ -261,7 +281,7 @@ async def login(request: Request): return JSONResponse({"status": "error", "message": "User not found", "errorId": 4}, status_code=404) 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), "ExpiresAt": time.time() + accesLifeTime}, "accessTokenSecret", algorithm="HS256") + access = jwt.encode({"user_id": int(user.id), "ExpiresAt": time.time() + (accesLifeTime if not bot else accesLifeTimeBot)}, "accessTokenSecret", algorithm="HS256") if len(user.tokens) > 10: # If user have more than 10 tokens await db.token.delete_many(where={"user_id": user.id}) @@ -282,7 +302,7 @@ async def login(request: Request): @app.post("/api/refresh_token") # refresh token handler -@limiter.limit(f"10/minute") +@limiter.limit(dynamic_limit_provider) async def login(request: Request): body = await request.json() if "accessToken" not in body: # If token doesn't provided @@ -303,7 +323,7 @@ async def login(request: Request): @app.post("/api/logout") # logout handler -@limiter.limit(f"10/minute") +@limiter.limit(dynamic_limit_provider) async def login(request: Request, Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None): diff --git a/schema.prisma b/schema.prisma index b6f1463..c943748 100644 --- a/schema.prisma +++ b/schema.prisma @@ -21,6 +21,7 @@ model file { key String @default("") type String @default("") ext String @default("") + size Int @default(0) user_filename String @default("") uses_number Int @default(0) max_uses Int @default(0) diff --git a/web/script.js b/web/script.js index 6a15d23..f9126f3 100644 --- a/web/script.js +++ b/web/script.js @@ -45,7 +45,7 @@ function append_to_files_arr(data, id){ creation_date_div.id = "creation_date_div"; let cr_time = document.createElement("p"); - cr_time.innerHTML = data['creation_date']; + cr_time.innerHTML = data['creation_date'] + " " + (!data['size'] || data['size'] == "0B"? "" : data['size']); cr_time.id = "cr_time"; const button = document.createElement('button'); @@ -114,7 +114,7 @@ async function fetch_files(accessToken, len){ localStorage.setItem("accessToken", new_access); } try{ - let response = await axios.get(api_url + "/api/getFiles", { + let response = await axios.get(api_url + "/api/get_files", { headers: { 'Authorization': 'Bearer ' + accessToken }