diff --git a/.gitignore b/.gitignore index aad3bbc..524acd4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /__pycache__ /dataBase.db /.env -/tg \ No newline at end of file +/tg +/out \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c456616 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "FileUploaderBot"] + path = FileUploaderBot + url = https://github.com/kwixie1/FileUploaderBot.git diff --git a/FileUploaderBot b/FileUploaderBot new file mode 160000 index 0000000..2c0f5d9 --- /dev/null +++ b/FileUploaderBot @@ -0,0 +1 @@ +Subproject commit 2c0f5d9c7f5660ea101321c52f41197dfa552e91 diff --git a/README.md b/README.md index a221a6b..210b42b 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,10 @@ The file uploader is implemented in the Python programming language using FastAP ## 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 8 endpoint URLs: +Page redirection is handled through the nginx proxy server. The API consists of 2 pathes: - `/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 +- `/api/` – Main API endpoint ### 1.1 Authorization Errors @@ -30,6 +23,7 @@ This section will be referred to as `1.1` in the documentation. ```json { "status": "error", + "message": "Auth error", "auth_error": { "message": "error message", "errorId": @@ -39,13 +33,13 @@ This section will be referred to as `1.1` in the documentation. **List of errors:** -| errorId | message | Reasons | -| ------- | ----------------------------------------------------- | --------------------------------------------- | -| -1 | No Authorization header provided | The request is missing the `Authorization` header | +| errorId | message | Reasons | +| ------- | ------------------------------------------------------ | -------------------------------------------------- | +| -1 | No Authorization header provided | The request is missing the `Authorization` header | | -2 | Authorization header must have `Bearer ` format | The `Authorization` header has an incorrect format | -| -3 | Access token expired | The token has expired | -| -4 | Invalid access token | The token cannot be decrypted | -| -5 | Token not found | The token is not found | +| -3 | Access token expired | The token has expired | +| -4 | Invalid access token | The token cannot be decrypted | +| -5 | Token not found | The token is not found | ### 1.2 Basic API @@ -56,16 +50,19 @@ If the file type cannot be determined, the API returns the file in download mode #### Possible Errors -| Error Code | Description | Possible Reasons | -| ---------- | ----------------------------- | ------------------------------------------ | -| 404 | File not found | The file referenced by the code does not exist | +| Error Code | Description | Possible Reasons | +| ---------- | -------------- | ---------------------------------------------- | +| 404 | File not found | The file referenced by the code does not exist | ### Upload a file to the server -`POST /api/upload?include_ext=false` +`POST /api/upload/{group_id}?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**. +**Query params:** +> The `group_id` parameter indicates the group to which the file should be uploaded. The parameter can take the value `private`, which means that the file must be linked to a personal account. When passing the `group_id` parameter other than the `private` Authorization header is required. + **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. @@ -93,11 +90,13 @@ On successful execution, the API returns a `200` HTTP code along with a JSON res ``` #### Possible Errors -| Error Code | Description | Possible Reasons | -| ---------- | ------------------------------ | ---------------------------------------- | -| 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 | +| Error Code | message | Possible Reasons | +| ---------- | ----------------------------------- | ------------------------------------------------------------------------------ | +| 400 | No file uploaded | No file is given in the request body | +| 413 | File size exceeds the limit (100MB) | The file size exceeds 100MB | +| 400 | Invalid group id | `group_id` parameter contains non-numerical value | +| 404 | Group not found | The group wuth provided `group_id` not found | +| 403 | You are not in the group | Group is exists, but you are has not permissions to upload files in this group | ### Delete a file `GET /api/delete/?key=` @@ -105,10 +104,10 @@ Successful execution returns a `200` status code, removing the file from the ser #### Possible Errors -| Error Code | Description | Possible Reasons | -| ---------- | -------------------------- | ---------------------------------- | -| 404 | File not found | The file for deletion is not found | -| 400 | Invalid unique key | The provided unique key is invalid | +| Error Code | Description | Possible Reasons | +| ---------- | ------------------ | ---------------------------------- | +| 404 | File not found | The file for deletion is not found | +| 400 | Invalid unique key | The provided unique key is invalid | ### 1.2 Authorization API ### Login and register @@ -122,7 +121,7 @@ Both requests accept the same request body but have different errors. ```json { - "username": "Andcool", + "username": "My cool username", "password": "My cool password" } ``` @@ -142,22 +141,22 @@ Successful execution returns a `200` HTTP code, indicating successful registrati **Common for both requests:** -| errorId | HTTP code |message | Reasons | -| ------- | ----------|---------------------------------| --------------------------------------------------- | -| 2 | 400 | 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 | HTTP code | message | Reasons | -| ------- | ----------|------------------------------------------------| ----------------------------------------------| -| 1 | 400 |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 | HTTP code | message | Reasons | -| ------- | ----------|----------------------| -----------------------| -| 3 | 400 |Wrong password | Incorrect password | -| 4 | 404 |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 | ### Refreshe the token `POST /api/refresh_token` @@ -167,9 +166,9 @@ Successful execution returns a `200` HTTP code along with the `accessToken` fiel #### Possible Errors -| errorId | HTTP code | message | Reasons | -| ------- | ----------|-----------------------------| ------------------------------------------------ | -| 5 | 400 | No access token provided | The `accessToken` field is missing in the request | +| errorId | HTTP code | message | Reasons | +| ------- | --------- | ------------------------ | ------------------------------------------------- | +| 5 | 400 | No access token provided | The `accessToken` field is missing in the request | Errors described in section `1.1` may also occur. @@ -185,10 +184,12 @@ Errors described in section `1.1` may occur as well. ### Get a list of files. -`GET /api/get_files` +`GET /api/get_files/{group_id}` It takes the `Authorization` header containing the access token. Retrieves a list of all files associated with this account. +> The `group_id` parameter shows which group to get the files from. If the parameter is set to `private`, the files will be taken from a personal account, otherwise Authorization header is required + #### Response Example ```json @@ -196,6 +197,7 @@ Retrieves a list of all files associated with this account. "status": "success", "message": "files got successfully", "username": "My cool username", + "is_group_owner": true, "data": [ { "file_url": "4yn-8yjhsu", @@ -221,4 +223,237 @@ 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. +| Error Code | message | Possible Reasons | +| ---------- | ------------------------ | ------------------------------------------------------------------------------ | +| 400 | Invalid group id | `group_id` parameter contains non-numerical value | +| 404 | Group not found | The group wuth provided `group_id` not found | +| 403 | You are not in the group | Group is exists, but you are has not permissions to upload files in this group | + + +### Transfer local files to an account. +`POST /api/transfer` +It takes the `Authorization` header containing the access token. + +> The request transfers local files to the account. The endpoint takes files from the `data` field in the request body and sequentially binds each file to an account. If the binding of any file failed, then these files will be returned in the response body in the `unsuccess` field. The file is transferred by their `file_url` and `key` + +#### Request body example +```json +{ + "data":[ + { + "file_url": "4yn-8yjhsu", + "key": "6b9a1c1b-5594-4cb9-8d49-99a4c28782a1" + }, + { + "file_url": "4yn-8yjhsR", + "key": "6b9a1c1b-5594-4cb9-8d49-99a4c28782a1" + } + ] +} +``` + +#### Response example + +```json +{ + "status": "success", + "message": "transfered", + "unsuccess": [ + { + "file_url": "4yn-8yjhsR", + "key": "6b9a1c1b-5594-4cb9-8d49-99a4c28782a1" + } + ] +} + +``` + +#### Possible Errors + +Errors described in section `1.1` may occur as well. +| Error Code | message | Possible Reasons | +| ---------- | ------------------------------- | ---------------------------------------------- | +| 400 | Couldn't parse request body | No request body provided | +| 400 | No `data` field in request body | Couldn't not find `data` field in request body | + +## Groups + +### Create file group +`POST /api/create_group` +It takes the `Authorization` header containing the access token. +> Creates a group with the passed name. The maximum length of a group name is 50 characters. + +#### Request body example +```json +{ + "group_name": "New group" +} +``` + +#### Response example + +```json +{ + "status": "success", + "message": "created", + "name": "New group", + "group_id": 12345678, +} +``` + +#### Possible Errors + +Errors described in section `1.1` may occur as well. +| Error Code | message | Possible Reasons | +| ---------- | ------------------------------------- | ---------------------------------------------------- | +| 400 | No `group_name` provided | Couldn't not find `group_name` field in request body | +| 400 | Group name length exceeded (50 chars) | Too long group name | + +### Delete file group +`DELETE /api/delete_group/{group_id}` +It takes the `Authorization` header containing the access token. +> Deletes a group with the passed `group_id`. + + +#### Response example + +```json +{ + "status": "success", + "message": "deleted" +} +``` + +#### Possible Errors + +Errors described in section `1.1` may occur as well. +| Error Code | message | Possible Reasons | +| ---------- | -------------------------------------------------- | -------------------------------------- | +| 404 | Group not found | Group with passed `group_id` not found | +| 403 | You dont have any permissions to delete this group | You are not owner of this group | + +### Generate a new invite link +`GET /api/generate_invite/{group_id}` +It takes the `Authorization` header containing the access token. +> Generates a invite link for group with the passed `group_id`. + + +#### Response example + +```json +{ + "status": "success", + "message": "created", + "invite_link": "https://fu.andcool.ru/invite/DSAfd4-hpoqFDFj" +} +``` + +#### Possible Errors + +Errors described in section `1.1` may occur as well. +| Error Code | message | Possible Reasons | +| ---------- | ----------------------------- | -------------------------------------- | +| 404 | Group not found | Group with passed `group_id` not found | +| 403 | You dont have any permissions | You are not owner of this group | + +### Join to a group +`POST /api/join/{invite_link}` +It takes the `Authorization` header containing the access token. +> Joins to a group by passed `invite_link`. + + +#### Response example + +```json +{ + "status": "success", + "message": "created", + "name": "New group", + "group_id": 12345678, +} +``` + +#### Possible Errors + +Errors described in section `1.1` may occur as well. +| Error Code | message | Possible Reasons | +| ---------- | ---------------------------- | ----------------------------- | +| 404 | Invite link not found | Could not found a invite link | +| 400 | You are already in the group | You are already in the group | + + +### Leave from group +`POST /api/leave/{group_id}` +It takes the `Authorization` header containing the access token. +> Leaves from group with passed `group_id` + + +#### Response example +```json +{ + "status": "success", + "message": "leaved" +} +``` + +#### Possible Errors + +Errors described in section `1.1` may occur as well. +| Error Code | message | Possible Reasons | +| ---------- | ------------------------ | -------------------------------------- | +| 404 | Group not found | Group with passed `group_id` not found | +| 400 | You are not in the group | You are not in the group | + + +### Get info about invite link +`GET /api/invite_info/{invite_link}` +It takes the `Authorization` header containing the access token. +> Gives an information about group about invite link. + + +#### Response example +```json +{ + "status": "success", + "message": "created", + "name": "New group", + "group_id": 12345678, +} +``` + +#### Possible Errors + +Errors described in section `1.1` may occur as well. +| Error Code | message | Possible Reasons | +| ---------- | --------------------- | ----------------------------- | +| 404 | Invite link not found | Could not found a invite link | + + +### Get all group from account +`GET /api/get_groups` +It takes the `Authorization` header containing the access token. +> Get list of groups linked to an account. + + +#### Response example +```json +{ + "status": "success", + "message": "groups got successfully", + "groups": [ + { + "name": "Bim-Bim", + "group_id": 12345679 + }, + { + "name": "Bam-Bam", + "group_id": 12345677 + } + ] +} +``` + +#### Possible Errors + +Errors described in section `1.1` may occur as well. diff --git a/accept_invite.html b/accept_invite.html new file mode 100644 index 0000000..77c0621 --- /dev/null +++ b/accept_invite.html @@ -0,0 +1,213 @@ + + + + + File uploader + + + + + + + +
+

File uploader

+ +
+
+

Loading...

+

+ +
+
+ + + + \ No newline at end of file diff --git a/imports.py b/imports.py new file mode 100644 index 0000000..fc02171 --- /dev/null +++ b/imports.py @@ -0,0 +1,23 @@ +from fastapi import FastAPI, UploadFile, Request, Header +from fastapi.responses import JSONResponse, FileResponse, Response +from typing import Annotated, Union +import uvicorn +from config import * +import aiohttp +import utils +from slowapi.errors import RateLimitExceeded +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.util import get_remote_address +from fastapi.middleware.cors import CORSMiddleware +import time +import aiofiles +from prisma import Prisma +import uuid +import os +from datetime import datetime +from dotenv import load_dotenv +import jwt +import bcrypt +import random + +rate_limit_exceeded_handler = _rate_limit_exceeded_handler diff --git a/main.py b/main.py index 3420c3f..edb95c0 100644 --- a/main.py +++ b/main.py @@ -2,29 +2,11 @@ created by AndcoolSystems, 2023-2024 """ -from fastapi import FastAPI, UploadFile, Request, Header -from fastapi.responses import JSONResponse, FileResponse, Response -from typing import Annotated, Union -import uvicorn -from config import * -import aiohttp -import utils -from slowapi.errors import RateLimitExceeded -from slowapi import Limiter, _rate_limit_exceeded_handler -from slowapi.util import get_remote_address -from fastapi.middleware.cors import CORSMiddleware -import time -import aiofiles -from prisma import Prisma -import uuid -import os -from datetime import datetime -from dotenv import load_dotenv -import jwt -import bcrypt +from imports import * + def custom_key_func(request: Request): - if get_remote_address(request) == os.getenv('SERVER_IP'): + if get_remote_address(request) == os.getenv("SERVER_IP"): return "bots" return "user" @@ -46,7 +28,7 @@ def dynamic_limit_provider_upload(key: str): db = Prisma() load_dotenv() app.state.limiter = limiter -app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) +app.add_exception_handler(RateLimitExceeded, rate_limit_exceeded_handler) app.add_middleware( # Disable CORS CORSMiddleware, @@ -56,104 +38,186 @@ def dynamic_limit_provider_upload(key: str): allow_headers=["*"], ) + @app.on_event("startup") async def startup_event(): await db.connect() # Connecting to database print("Connected to Data Base") -@app.get("/api") # Get file handler +@app.get("/api") # root api endpoint @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) + 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): if not Authorization: # If token doesn't provided return None, {"message": "No Authorization header provided", "errorId": -1} - + token_header = Authorization.split(" ") if len(token_header) != 2: # If token have unsupported format - return None, {"message": "Authorization header must have `Bearer ` format", "errorId": -2} - + return None, { + "message": "Authorization header must have `Bearer ` format", + "errorId": -2, + } + try: - token = jwt.decode(token_header[1], "accessTokenSecret", algorithms=["HS256"]) # Decode token + token = jwt.decode( + token_header[1], "accessTokenSecret", algorithms=["HS256"] + ) # Decode token except jwt.exceptions.DecodeError: return None, {"message": "Invalid access token", "errorId": -4} - - token_db = await db.token.find_first(where={"accessToken": token_header[1]}) # Find token in db + + token_db = await db.token.find_first( + where={"accessToken": token_header[1]}, include={"user": True} + ) # 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 +@app.get("/invite/{group_id}") # invite page handler +async def invite(group_id: str, request: Request): + async with aiofiles.open("accept_invite.html", mode="rb") as f: + return Response(await f.read(), media_type="text/html", status_code=200) + + +@app.post("/api/upload/{group_id}") # File upload handler @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 - return JSONResponse(content={"status": "error", "message": "No file uploaded"}, status_code=400) +async def upload_file( + group_id: str, + file: UploadFile, + request: Request, + include_ext: bool = False, + max_uses: int = 0, + Authorization: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): - if file.filename.find(".") == -1: # Check, if the file has a extension - return JSONResponse(content={"status": "error", "message": "Bad file extension"}, status_code=400) + if not file: # Check, if the file is uploaded + return JSONResponse( + content={"status": "error", "message": "No file uploaded"}, status_code=400 + ) + + if file.size > 100 * 1024 * 1024: # 100MB limit + return JSONResponse( + content={ + "status": "error", + "message": "File size exceeds the limit (100MB)", + }, + status_code=413, + ) - if file.size > 100 * 1024 * 1024: # 100MB limit - return JSONResponse(content={"status": "error", "message": "File size exceeds the limit (100MB)"}, status_code=413) - if max_uses > 10000: - return JSONResponse(content={"status": "error", "message": "Invalid max_uses parameter"}, status_code=400) - - user_id = -1 + return JSONResponse( + content={"status": "error", "message": "Invalid max_uses parameter"}, + status_code=400, + ) + saved_to_account = False + user_id = -1 token_db, auth_error = await check_token(Authorization) # Check token if token_db: # If token is okay - user_id = token_db.user_id saved_to_account = True + user_id = token_db.user.id + + if group_id != "private": + if not token_db: # If token is not valid + return JSONResponse( + content={ + "status": "error", + "message": "Auth error", + "auth_error": auth_error, + }, + status_code=401, + ) + if not group_id.isnumeric(): + return JSONResponse( + content={"status": "error", "message": "Invalid group id"}, + status_code=400, + ) + + group = await db.group.find_first( + where={"group_id": group_id}, include={"members": True} + ) + if not group: + return JSONResponse( + content={"status": "error", "message": "Group not found"}, + status_code=404, + ) + if token_db.user not in group.members: + return JSONResponse( + content={"status": "error", "message": "You are not in the group"}, + status_code=403, + ) + else: + group_id = -1 key = str(uuid.uuid4()) # Generate unique delete key - ext = "." + file.filename.split(".")[-1].lower() # Get file extension + ext = ("." + file.filename.split(".")[-1].lower()) if file.filename.find(".") != -1 else "" # Get file extension fid = utils.generate_token(10) + (ext if include_ext else "") # Generate file url - fn = str(uuid.uuid4()) + ext # Generate file name + fn = str(uuid.uuid4()) + ext # Generate file name async with aiofiles.open(f"uploads/{fn}", "wb") as f: # Save file locally await f.write(file.file.read()) now = datetime.now() - created = await db.file.create({ # Creating a file record - "user_id": user_id, - "created_date": f"{now.day}.{now.month}.{now.year} {now.hour}:{now.minute}:{now.second}", - "url": fid, - "filename": f"uploads/{fn}", - "craeted_at": time.time(), - "last_watched": time.time(), - "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 - }) - - user_filename = created.user_filename[:50] + ("..." if len(created.user_filename) > 50 else "") - return JSONResponse(content={"status": "success", - "message": "File uploaded successfully", - "file_url": created.url, - "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, - "auth_error": auth_error}, status_code=200) + created = await db.file.create( + { # Creating a file record + "user_id": user_id, + "group_id": int(group_id), + "created_date": f"{now.day}.{now.month}.{now.year} {now.hour}:{now.minute}:{now.second}", + "url": fid, + "filename": f"uploads/{fn}", + "craeted_at": time.time(), + "last_watched": time.time(), + "key": key, + "type": ( + filetypes.get(ext[1:], default) if ext and ext.lower()[1:] in filetypes else "download" + ), + "ext": ext, + "size": file.size, + "user_filename": file.filename, + "max_uses": max_uses, + } + ) + + user_filename = created.user_filename[:50] + ( + "..." if len(created.user_filename) > 50 else "" + ) + return JSONResponse( + content={ + "status": "success", + "message": "File uploaded successfully", + "file_url": created.url, + "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, + "username": None if not token_db and group_id != "private" else token_db.user.username, + "craeted_at": created.craeted_at, + "synced": saved_to_account, + "auth_error": auth_error, + }, + status_code=200, + ) @app.get("/file/{url}") # Get file handler @@ -161,180 +225,716 @@ async def upload_file(file: UploadFile, request: Request, include_ext: bool = Fa @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: + if not result: async with aiofiles.open("404.html", mode="rb") as f: - return Response(await f.read(), media_type="text/html", status_code=404) # if file does'n exists - - print(request.headers.get('CF-IPCountry')) - - if 'sec-fetch-dest' in request.headers: - if request.headers.get('sec-fetch-dest') == 'document': - result = await db.file.update(where={"id": result.id}, - data={"last_watched": time.time(), "uses_number": result.uses_number + 1}) # Update last watched record + return Response( + await f.read(), media_type="text/html", status_code=404 + ) # if file does'n exists + + print(request.headers.get("CF-IPCountry")) + + if "sec-fetch-dest" in request.headers: + if request.headers.get("sec-fetch-dest") == "document": + result = await db.file.update( + where={"id": result.id}, + data={ + "last_watched": time.time(), + "uses_number": result.uses_number + 1, + }, + ) # Update last watched record else: - result = await db.file.update(where={"id": result.id}, - data={"last_watched": time.time(), "uses_number": result.uses_number + 1}) + result = await db.file.update( + where={"id": result.id}, + data={"last_watched": time.time(), "uses_number": result.uses_number + 1}, + ) if result.max_uses < result.uses_number and result.max_uses != 0: await delete_file(result.url, result.key) return JSONResponse(content="File not found!", status_code=404) - + if result.type != "download": # If File extension recognized async with aiofiles.open(result.filename, mode="rb") as f: - return Response(await f.read(), media_type=result.type) # Send file with "Content-type" header + return Response( + await f.read(), media_type=result.type + ) # Send file with "Content-type" header else: # If file extension doesn't recognized - return FileResponse(path=result.filename, filename=result.user_filename, media_type=result.type) # Send file as FileResponse + return FileResponse( + path=result.filename, filename=result.user_filename, media_type=result.type + ) # Send file as FileResponse @app.get("/api/delete/{url}") # File delete handler async def delete_file(url: str, key: str = ""): result = await db.file.find_first(where={"url": url}) # Get file record by url - if not result: return JSONResponse(content={"status": "error", "message": "File not found"}, status_code=200) # if file does'n exists + if not result: + return JSONResponse( + content={"status": "error", "message": "File not found"}, status_code=200 + ) # if file does'n exists if result.key == key: # If provided key matched with key from database record os.remove(result.filename) # Delete file - await db.file.delete(where={"id": result.id}) # Delete file record from database - - async with aiohttp.ClientSession("https://api.cloudflare.com") as session: # Clear file cache from CloudFlare - async with session.post(f"/client/v4/zones/{os.getenv('ZONE_ID')}/purge_cache", - json={"files": ["https://fu.andcool.ru/file/" + result.url]}, - headers={"Authorization": "Bearer " + os.getenv('KEY')}): pass - - return JSONResponse(content={"status": "success", "message": "deleted"}, status_code=200) + await db.file.delete( + where={"id": result.id} + ) # Delete file record from database + + async with aiohttp.ClientSession( + "https://api.cloudflare.com" + ) as session: # Clear file cache from CloudFlare + async with session.post( + f"/client/v4/zones/{os.getenv('ZONE_ID')}/purge_cache", + json={"files": ["https://fu.andcool.ru/file/" + result.url]}, + headers={"Authorization": "Bearer " + os.getenv("KEY")}, + ): + pass + + return JSONResponse( + content={"status": "success", "message": "deleted"}, status_code=200 + ) else: # If provided key doesn't matched with key from database record - return JSONResponse(content={"status": "error", "message": "invalid unique key"}, status_code=400) + return JSONResponse( + content={"status": "error", "message": "invalid unique key"}, + status_code=400, + ) -@app.get("/api/getFiles") # get files handler -@app.get("/api/get_files") # get files handler +@app.get("/api/getFiles/{group_id}") # get files handler +@app.get("/api/get_files/{group_id}") # get files handler @limiter.limit(dynamic_limit_provider) -async def getFiles(request: Request, - Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None): +async def getFiles( + group_id: str, + request: Request, + Authorization: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): token_db, auth_error = await check_token(Authorization) # Check token validity if not token_db: # If token is not valid - return JSONResponse(content={"status": "error", "auth_error": auth_error}, status_code=401) - - files = await db.file.find_many(where={"user_id": token_db.user_id}) # Get all user files from db - user = await db.user.find_first(where={"id": token_db.user_id}) # Get all user files from db + return JSONResponse( + content={ + "status": "error", + "message": "Auth error", + "auth_error": auth_error, + }, + status_code=401, + ) + + user = await db.user.find_first( + where={"id": token_db.user_id} + ) # Get user files from db + + if group_id == "private": + files = await db.file.find_many( + where={"user_id": user.id, "group_id": -1} + ) # Get all user files from db + else: + if not group_id.isnumeric(): + return JSONResponse( + content={"status": "error", "message": "Invalid group id"}, + status_code=400, + ) + + group = await db.group.find_first( + where={"group_id": group_id}, include={"members": True} + ) + if not group: + return JSONResponse( + content={"status": "error", "message": "Group not found"}, + status_code=404, + ) + if user not in group.members: + return JSONResponse( + {"status": "error", "message": "You are not in the group"}, + status_code=400, + ) + + files = await db.file.find_many(where={"group_id": group_id}) + files_response = [] for file in files: - files_response.append({ - "file_url": file.url, - "file_url_full": "https://fu.andcool.ru/file/" + file.url, - "key": file.key, - "ext": file.ext, - "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) + user_filename = file.user_filename[:50] + ( + "..." if len(file.user_filename) > 50 else "" + ) + usr = (await db.user.find_first(where={"id": file.user_id})).username if group_id != "private" else None + files_response.append( + { + "file_url": file.url, + "file_url_full": "https://fu.andcool.ru/file/" + file.url, + "key": file.key, + "ext": file.ext, + "user_filename": user_filename, + "creation_date": file.created_date, + "craeted_at": file.craeted_at, + "size": utils.calculate_size(file.size), + "username": usr, + "synced": True, + } + ) + return JSONResponse( + content={ + "status": "success", + "message": "messages got successfully", + "username": user.username, + "is_group_owner": ( + None if group_id == "private" else group.admin_id == token_db.user_id + ), + "data": files_response, + }, + status_code=200, + ) @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 - return JSONResponse({"status": "error", "message": "No username/password provided", "errorId": 2}, status_code=400) - - user = await db.user.find_first(where={'username': body['username']}) # Find same username in db + if ( + "username" not in body or "password" not in body + ): # If request body doesn't have username and password field + return JSONResponse( + { + "status": "error", + "message": "No username/password provided", + "errorId": 2, + }, + status_code=400, + ) + + user = await db.user.find_first( + where={"username": body["username"]} + ) # Find same username in db if user: # If iser already exists - return JSONResponse({"status": "error", "message": "An account with this name is already registered", "errorId": 1}, status_code=400) + return JSONResponse( + { + "status": "error", + "message": "An account with this name is already registered", + "errorId": 1, + }, + status_code=400, + ) salt = bcrypt.gensalt() # Encrypt password - hashed = bcrypt.hashpw(bytes(str(body['password']), "utf-8"), salt) + hashed = bcrypt.hashpw(bytes(str(body["password"]), "utf-8"), salt) user = await db.user.create( # Create user record in db - { - "username": str(body['username']), - "password": str(hashed.decode('utf-8')) - } + {"username": str(body["username"]), "password": str(hashed.decode("utf-8"))} ) - 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 - "accessToken": access, - 'user': { - 'connect': { - 'id': user.id, + 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 + "accessToken": access, + "user": { + "connect": { + "id": user.id, + }, }, } - }) - return JSONResponse({"status": "success", "accessToken": access, "username": body['username'], "message": "registred"}, status_code=200) + ) + return JSONResponse( + { + "status": "success", + "accessToken": access, + "username": body["username"], + "message": "registred", + }, + 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() - if "username" not in body or \ - "password" not in body: # If request body doesn't have username and password field - return JSONResponse({"status": "error", "message": "No username/password provided", "errorId": 2}, status_code=400) - - user = await db.user.find_first(where={'username': body['username']}, include={"tokens": True}) # Find same username in db - if not user: # If user doesn't exists - return JSONResponse({"status": "error", "message": "User not found", "errorId": 4}, status_code=404) + if ( + "username" not in body or "password" not in body + ): # If request body doesn't have username and password field + return JSONResponse( + { + "status": "error", + "message": "No username/password provided", + "errorId": 2, + }, + status_code=400, + ) - 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 if not bot else accesLifeTimeBot)}, "accessTokenSecret", algorithm="HS256") + user = await db.user.find_first( + where={"username": body["username"]}, include={"tokens": True} + ) # Find same username in db + if not user: # If user doesn't exists + 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 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}) - - await db.token.create({ # Create token record in db - "accessToken": access, - 'user': { - 'connect': { - 'id': user.id, - } - }}) - return {"status": "success", + await db.token.create( + { # Create token record in db "accessToken": access, - "username": user.username, - "message": "logged in with password"} + "user": { + "connect": { + "id": user.id, + } + }, + } + ) + + return { + "status": "success", + "accessToken": access, + "username": user.username, + "message": "logged in with password", + } else: # If password doesn't match - return JSONResponse({"status": "error", "message": "Wrong password", "errorId": 3}, status_code=400) + return JSONResponse( + {"status": "error", "message": "Wrong password", "errorId": 3}, + status_code=400, + ) @app.post("/api/refresh_token") # refresh token handler @limiter.limit(dynamic_limit_provider) -async def login(request: Request): +async def refresh_token(request: Request): body = await request.json() if "accessToken" not in body: # If token doesn't provided - return JSONResponse({"status": "error", "message": "No access token provided", "errorId": 5}, status_code=400) - - token_db, auth_error = await check_token(body["accessToken"]) # Check token validity + return JSONResponse( + {"status": "error", "message": "No access token provided", "errorId": 5}, + status_code=400, + ) + + token_db, auth_error = await check_token( + body["accessToken"] + ) # Check token validity if not token_db: # If token is not valid - return JSONResponse(content={"status": "error", "auth_error": auth_error}, status_code=401) - - access = jwt.encode({"user_id": int(token_db.user_id), "ExpiresAt": time.time() + accesLifeTime}, - "accessTokenSecret", algorithm="HS256") # Generate new token - await db.token.update(where={"id": token_db.id}, # Replace old token - data={"accessToken": access}) - - return {"status": "success", - "accessToken": access, - "message": "token refreshed"} + return JSONResponse( + content={ + "status": "error", + "message": "Auth error", + "auth_error": auth_error, + }, + status_code=401, + ) + + access = jwt.encode( + {"user_id": int(token_db.user_id), "ExpiresAt": time.time() + accesLifeTime}, + "accessTokenSecret", + algorithm="HS256", + ) # Generate new token + await db.token.update( + where={"id": token_db.id}, data={"accessToken": access} # Replace old token + ) + + return {"status": "success", "accessToken": access, "message": "token refreshed"} @app.post("/api/logout") # logout handler @limiter.limit(dynamic_limit_provider) -async def login(request: Request, - Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None): - +async def logout( + request: Request, + Authorization: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): + token_db, auth_error = await check_token(Authorization) # Check token validity if not token_db: # If token is not valid - return JSONResponse(content={"status": "error", "auth_error": auth_error}, status_code=401) + return JSONResponse( + content={ + "status": "error", + "message": "Auth error", + "auth_error": auth_error, + }, + status_code=401, + ) await db.token.delete(where={"id": token_db.id}) # Delete token record from db - return {"status": "success", - "message": "logged out"} + return {"status": "success", "message": "logged out"} + + +@app.post("/api/transfer") # transfer handler +@limiter.limit(dynamic_limit_provider) +async def transfer( + request: Request, + Authorization: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): + + token_db, auth_error = await check_token(Authorization) # 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, + ) + + try: + body = await request.json() + except: + return JSONResponse( + content={"status": "error", "message": "Couldn't parse request body"}, + status_code=400, + ) + + if "data" not in body: + return JSONResponse( + content={"status": "error", "message": "No `data` field in request body"}, + status_code=400, + ) + + non_success = [] + for requested_file in body["data"]: + try: + file = await db.file.find_first(where={"url": requested_file["file_url"]}) + if not file or file.key != requested_file["key"]: + non_success.append(requested_file) + continue + + await db.file.update( + where={"id": file.id}, data={"user_id": token_db.user_id} + ) + except: + non_success.append(requested_file) + + return {"status": "success", "message": "transfered", "unsuccess": non_success} + + +# --------------------------------------Groups------------------------------------------ + + +@app.post("/api/create_group") # create group handler +@limiter.limit(dynamic_limit_provider) +async def create_group( + request: Request, + Authorization: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): + + body = await request.json() + if "group_name" not in body: # If token doesn't provided + return JSONResponse( + {"status": "error", "message": "No `group_name` provided"}, status_code=400 + ) + + token_db, auth_error = await check_token(Authorization) # 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, + ) + + if len(body["group_name"]) > 50: + return JSONResponse( + content={ + "status": "error", + "message": "Group name length exceeded (50 chars)", + }, + status_code=400, + ) + group = await db.group.create( + data={ + "name": body["group_name"], + "group_id": random.randint(10000000, 99999999), + "admin_id": token_db.user_id, + "members": { + "connect": {"id": token_db.user_id}, + }, + } + ) + return { + "status": "success", + "message": "created", + "name": group.name, + "group_id": group.group_id, + } + + +@app.delete("/api/delete_group/{group_id}") # delete group handler +@limiter.limit(dynamic_limit_provider) +async def delete_group( + group_id: int, + request: Request, + Authorization: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): + + token_db, auth_error = await check_token(Authorization) # 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, + ) + + group = await db.group.find_first(where={"group_id": group_id}) + if not group: + return JSONResponse( + {"status": "error", "message": "Group not found"}, status_code=404 + ) + + if group.admin_id != token_db.user_id: + return JSONResponse( + { + "status": "error", + "message": "You dont have any permissions to delete this group", + }, + status_code=403, + ) + + await db.group.delete(where={"id": group.id}) + + return {"status": "success", "message": "deleted"} + + +@app.get("/api/generate_invite/{group_id}") # generate invite handler +@limiter.limit(dynamic_limit_provider) +async def generate_invite( + group_id: int, + request: Request, + Authorization: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): + + token_db, auth_error = await check_token(Authorization) # 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, + ) + + group = await db.group.find_first(where={"group_id": group_id}) + if not group: + return JSONResponse( + {"status": "error", "message": "Group not found"}, status_code=404 + ) + + if group.admin_id != token_db.user_id: + return JSONResponse( + { + "status": "error", + "message": "You dont have any permissions", + }, + status_code=403, + ) + + invite = await db.invitements.create( + data={"data": utils.generate_token(15), "group": {"connect": {"id": group.id}}} + ) + + return { + "status": "success", + "message": "created", + "invite_link": f"https://fu.andcool.ru/invite/{invite.data}", + } + + +@app.post("/api/join/{invite_link}") # join handler +@limiter.limit(dynamic_limit_provider) +async def delete_group( + invite_link: str, + request: Request, + Authorization: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): + + token_db, auth_error = await check_token(Authorization) # 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, + ) + + invite = await db.invitements.find_first( + where={"data": invite_link}, include={"group": True} + ) + if not invite: + return JSONResponse( + {"status": "error", "message": "Invite link not found"}, status_code=404 + ) + + group = await db.group.find_first( + where={"id": invite.group_id}, include={"members": True} + ) + if token_db.user in group.members: + return JSONResponse( + {"status": "error", "message": "You are already in the group"}, + status_code=400, + ) + + await db.group.update( + data={ + "members": { + "connect": {"id": token_db.user_id}, + } + }, + where={"id": group.id}, + ) + + await db.invitements.delete(where={"id": invite.id}) + + return { + "status": "success", + "message": "Joined", + "name": group.name, + "group_id": group.group_id, + } + + +@app.get("/api/invite_info/{invite_link}") # invite info handler +@limiter.limit(dynamic_limit_provider) +async def delete_group( + invite_link: str, + request: Request, + Authorization: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): + + token_db, auth_error = await check_token(Authorization) # 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, + ) + + invite = await db.invitements.find_first( + where={"data": invite_link}, include={"group": True} + ) + if not invite: + return JSONResponse( + {"status": "error", "message": "Invite link not found"}, status_code=404 + ) + + return { + "status": "success", + "message": "Info got successfully", + "name": invite.group.name, + "group_id": invite.group.group_id, + } + + +@app.post("/api/leave/{group_id}") # leave handler +@limiter.limit(dynamic_limit_provider) +async def delete_group( + group_id: int, + request: Request, + Authorization: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): + + token_db, auth_error = await check_token(Authorization) # 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, + ) + + group = await db.group.find_first( + where={"group_id": group_id}, include={"members": True} + ) + if not group: + return JSONResponse( + {"status": "error", "message": "Group not found"}, status_code=404 + ) + + if token_db.user not in group.members: + return JSONResponse( + {"status": "error", "message": "You are not in the group"}, status_code=400 + ) + + await db.group.update( + data={"members": {"disconnect": {"id": token_db.user_id}}}, + where={"id": group.id}, + ) + + return {"status": "success", "message": "leaved"} + + +@app.get("/api/get_groups") # get groups handler +@limiter.limit(dynamic_limit_provider) +async def get_groups( + request: Request, + Authorization: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None, +): + + token_db, auth_error = await check_token(Authorization) # 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, + ) + + user = await db.user.find_first( + where={"id": token_db.user_id}, include={"groups": True} + ) + groups = [] + for group in user.groups: + groups.append( + { + "name": group.name, + "group_id": group.group_id, + } + ) + return {"status": "success", "message": "groups got successfully", "groups": groups} if __name__ == "__main__": # Start program diff --git a/schema.prisma b/schema.prisma index c943748..9a22ff1 100644 --- a/schema.prisma +++ b/schema.prisma @@ -13,6 +13,7 @@ generator db { model file { id Int @id @default(autoincrement()) user_id Int @default(-1) + group_id BigInt @default(-1) created_date String @default("") url String @default("") filename String @default("") @@ -32,11 +33,30 @@ model User { username String @default("") password String @default("") tokens Token[] + groups Group[] } model Token{ - id Int @id @default(autoincrement()) - accessToken String - user User @relation(fields: [user_id], references: [id]) - user_id Int + id Int @id @default(autoincrement()) + accessToken String @default("") + user User @relation(fields: [user_id], references: [id]) + user_id Int @default(0) } + +model Group{ + id Int @id @default(autoincrement()) + name String @default("") + group_id BigInt @default(0) + admin_id Int @default(0) + members User[] + invitements Invitements[] +} + +model Invitements{ + id Int @id @default(autoincrement()) + data String @default("") + group Group @relation(fields: [group_id], references: [id]) + group_id Int @default(0) +} + + diff --git a/utils.py b/utils.py index 1bdf7b6..1ea0942 100644 --- a/utils.py +++ b/utils.py @@ -1,9 +1,11 @@ import random + def generate_token(length): base = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789-+=" return "".join([random.choice(base) for x in range(length)]) + def calculate_size(size: int): units = ["B", "KB", "MB", "GB", "TB"] unit_iteration = 0 @@ -14,4 +16,3 @@ def calculate_size(size: int): unit_iteration += 1 return f"{round(calculated_size, 2)}{units[unit_iteration]}" - diff --git a/web/index.html b/web/index.html index f4ebb4b..e7b86a6 100644 --- a/web/index.html +++ b/web/index.html @@ -7,6 +7,9 @@ + + + @@ -36,6 +39,9 @@ stop_anim(); } } + //const urlParams = new URLSearchParams(window.location.search); + //const myParam = urlParams.get('myParam'); + //console.log(myParam);
@@ -72,6 +78,30 @@

File uploader

Choose file + + + +

diff --git a/web/res/external-link.svg b/web/res/external-link.svg new file mode 100644 index 0000000..45f4011 --- /dev/null +++ b/web/res/external-link.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/web/res/user_plus.svg b/web/res/user_plus.svg new file mode 100644 index 0000000..d4be380 --- /dev/null +++ b/web/res/user_plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/script.js b/web/script.js index f9126f3..69d1fad 100644 --- a/web/script.js +++ b/web/script.js @@ -3,12 +3,12 @@ let api_file_url = "/file/"; let api_url = "https://fu.andcool.ru"; //let api_url = "http://127.0.0.1:8080"; -async function delete_file(data, id){ +async function delete_file(data, id) { let confirmed = confirm("Delete it? It will be impossible to restore the file!"); - if (confirmed){ + if (confirmed) { let response = await axios.get(api + "/api/delete/" + data.file_url + "?key=" + data.key); - if (response.status == 200){ - if (!data.synced){ + if (response.status == 200) { + if (!data.synced) { let old_data = JSON.parse(localStorage.getItem("file_history") || "[]"); old_data.splice(id, 1); localStorage.setItem("file_history", JSON.stringify(old_data)); @@ -19,45 +19,45 @@ async function delete_file(data, id){ } -function append_to_files_arr(data, id){ - let table = document.getElementById('files_table'); +function append_to_files_arr(data, id) { + let table = document.getElementById('files_table'); - // Insert a row at the start of table - let newRow = table.insertRow(0); + // Insert a row at the start of table + let newRow = table.insertRow(0); newRow.id = "row_" + id; - - // Insert a cell at the end of the row - let newCell = newRow.insertCell(); - let newCell2 = newRow.insertCell(); - // Append a text node to the cell - let url = document.createElement("a"); - url.innerHTML = data['file_url_full']; + newRow.className = "tr"; + + // Insert a cell at the end of the row + let newCell = newRow.insertCell(); + let newCell2 = newRow.insertCell(); + // Append a text node to the cell + let url = document.createElement("p"); + url.innerHTML = data['file_url_full']; url.id = "url"; - url.onclick = function(){navigator.clipboard.writeText(data['file_url_full']);} - //url.href = data['file_url_full']; - //url.target = "_blank"; + url.onclick = function () { navigator.clipboard.writeText(data['file_url_full']); } let filename = document.createElement("p"); - filename.innerHTML = data['user_filename']; + filename.innerHTML = data['user_filename']; filename.id = "filename"; let creation_date_div = document.createElement("div"); creation_date_div.id = "creation_date_div"; let cr_time = document.createElement("p"); - cr_time.innerHTML = data['creation_date'] + " " + (!data['size'] || data['size'] == "0B"? "" : data['size']); + cr_time.innerHTML = data['creation_date'] + " " + (!data['size'] || data['size'] == "0B" ? "" : data['size']); cr_time.id = "cr_time"; - const button = document.createElement('button'); - button.innerHTML = 'Delete'; - button.onclick = function(){delete_file(data, id);} + const button = document.createElement('button'); + button.innerHTML = 'Delete'; + button.className = "button" + button.onclick = function () { delete_file(data, id); } let online = document.createElement("img"); online.className = "online"; - if (data.synced){ + if (data.synced) { online.src = "./res/globe_on.png"; online.title = "Synchronized with the server"; - }else{ + } else { online.src = "./res/globe_off.png"; online.title = "Stored on local browser"; } @@ -69,10 +69,14 @@ function append_to_files_arr(data, id){ a_btn.title = "Open in new tab"; let href_img = document.createElement("img"); - href_img.src = "./res/external-link.png"; - + href_img.src = "./res/external-link.svg"; + href_img.id = "external"; a_btn.appendChild(href_img); + let username = document.createElement("p"); + username.innerHTML = data['username'] ? data['username'] + "'s file" : ""; + username.id = "username"; + let urls_div = document.createElement("div"); let url_link_div = document.createElement("div"); url_link_div.className = "url_link_div"; @@ -85,102 +89,376 @@ function append_to_files_arr(data, id){ creation_date_div.appendChild(cr_time); creation_date_div.appendChild(online); urls_div.appendChild(creation_date_div); + if (data["username"]) urls_div.appendChild(username); - newCell.appendChild(urls_div); - newCell2.appendChild(button); + newCell.appendChild(urls_div); + newCell2.appendChild(button); } -async function get_new_tokens(accessToken){ - try{ - let response = await axios.post(api_url + "/api/refresh_token", {'accessToken': "Bearer " + accessToken}, {}) + +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{ + } catch { return false; } } -async function fetch_files(accessToken, len){ +async function create_group() { + accessToken = localStorage.getItem("accessToken"); + if (!accessToken) return []; + if (!checkAccess(accessToken)) { + let new_access = await get_new_tokens(accessToken); + if (!new_access) { + localStorage.removeItem("accessToken"); + return []; + } + accessToken = new_access; + localStorage.setItem("accessToken", new_access); + } + try { + let name = document.getElementById('group_name').value; + if(name.length > 0 && name.length < 50){ + let response = await axios.post(api_url + "/api/create_group", { 'group_name': name }, { + headers: { + 'Authorization': 'Bearer ' + accessToken + } + }) + if (!response) return; + if (response.status == 200) { + let groups = document.getElementById('groups'); + groups.value = response.data.group_id; + localStorage.setItem('prev_group', response.data.group_id); + location.reload(); + }else{ + alert(response.data.message); + } + } + + + } catch (e) { + if (e.response && e.response.status == 401) { + localStorage.removeItem("accessToken"); + return []; + } + return []; + } +} + +async function create_invite() { + accessToken = localStorage.getItem("accessToken"); + if (!accessToken) return []; + if (!checkAccess(accessToken)) { + let new_access = await get_new_tokens(accessToken); + if (!new_access) { + localStorage.removeItem("accessToken"); + return []; + } + accessToken = new_access; + localStorage.setItem("accessToken", new_access); + } + try { + let gr_id = document.getElementById('groups').value; + let response = await axios.get(api_url + "/api/generate_invite/" + gr_id, { + headers: { + 'Authorization': 'Bearer ' + accessToken + } + }) + if (!response) return; + if (response.status == 200) { + let invite_link = document.getElementById('invite_link'); + invite_link.innerHTML = response.data.invite_link; + invite_link.onclick = () => {navigator.clipboard.writeText(response.data.invite_link)} + document.getElementById('invite_link_link').style.display = "block"; + }else{ + alert(response.data.message); + } + + + + } catch (e) { + if (e.response && e.response.status == 401) { + localStorage.removeItem("accessToken"); + return []; + } + return []; + } +} + + +async function delete_group() { + accessToken = localStorage.getItem("accessToken"); + if (!accessToken) return []; + if (!checkAccess(accessToken)) { + let new_access = await get_new_tokens(accessToken); + if (!new_access) { + localStorage.removeItem("accessToken"); + return []; + } + accessToken = new_access; + localStorage.setItem("accessToken", new_access); + } + try { + let gr_id = document.getElementById('groups').value; + if(confirm("Delete group? All files would be unlinked from the group! (Not deleted)")){ + let response = await axios.delete(api_url + "/api/delete_group/" + gr_id, { + headers: { + 'Authorization': 'Bearer ' + accessToken + } + }) + if (!response) return; + if (response.status == 200) { + localStorage.setItem('prev_group', "private"); + location.reload(); + }else{ + alert(response.data.message); + } + } + + + } catch (e) { + if (e.response && e.response.status == 401) { + localStorage.removeItem("accessToken"); + return []; + } + return []; + } +} + +async function leave_group() { + accessToken = localStorage.getItem("accessToken"); + if (!accessToken) return []; + if (!checkAccess(accessToken)) { + let new_access = await get_new_tokens(accessToken); + if (!new_access) { + localStorage.removeItem("accessToken"); + return []; + } + accessToken = new_access; + localStorage.setItem("accessToken", new_access); + } + try { + let gr_id = document.getElementById('groups').value; + if(confirm("Leave group?")){ + let response = await axios.post(api_url + "/api/leave/" + gr_id, {}, { + headers: { + 'Authorization': 'Bearer ' + accessToken + } + }) + if (!response) return; + if (response.status == 200) { + localStorage.setItem('prev_group', "private"); + location.reload(); + }else{ + alert(response.data.message); + } + } + + + } catch (e) { + if (e.response && e.response.status == 401) { + localStorage.removeItem("accessToken"); + return []; + } + return []; + } +} + + +async function transfer_func() { + accessToken = localStorage.getItem("accessToken"); if (!accessToken) return []; - if (!checkAccess(accessToken)){ + if (!checkAccess(accessToken)) { let new_access = await get_new_tokens(accessToken); - if (!new_access){ + if (!new_access) { localStorage.removeItem("accessToken"); return []; } - console.log(new_access); accessToken = new_access; localStorage.setItem("accessToken", new_access); } - try{ - let response = await axios.get(api_url + "/api/get_files", { + try { + let response = await axios.post(api_url + "/api/transfer", { 'data': JSON.parse(localStorage.getItem("file_history") || "[]") }, { headers: { 'Authorization': 'Bearer ' + accessToken } }) if (!response) return; - if (response.status == 401){ + if (response.status == 200) { + localStorage.setItem("file_history", JSON.stringify(response.data.unsuccess)); + location.reload(); + } + + + } catch (e) { + if (e.response && e.response.status == 401) { + localStorage.removeItem("accessToken"); + return []; + } + return []; + } +} + +async function fetch_groups() { + let accessToken = localStorage.getItem("accessToken"); + if (!accessToken) return []; + if (!checkAccess(accessToken)) { + let new_access = await get_new_tokens(accessToken); + if (!new_access) { localStorage.removeItem("accessToken"); return []; } + accessToken = new_access; + localStorage.setItem("accessToken", new_access); + } + try { + let response = await axios.get(api_url + "/api/get_groups", { + headers: { + 'Authorization': 'Bearer ' + accessToken + } + }) + if (!response) return; + + document.getElementById('groups_selector').style.display = "block"; + let prev_group = localStorage.getItem("prev_group"); + let groups = document.getElementById('groups'); + let value_finded = false; + for (const group of response.data.groups) { + let groupel = document.createElement("option"); + groupel.innerHTML = group.name; + groupel.value = group.group_id; + groups.appendChild(groupel); + if (group.group_id == prev_group){ + groups.value = prev_group; + value_finded = true; + } + } + if (!value_finded) localStorage.removeItem("prev_group"); + fetch_files(localStorage.getItem("accessToken")); + + } catch (e) { + if (e.response && e.response.status == 401) { + localStorage.removeItem("accessToken"); + return []; + } + return []; + } +} + +async function fetch_files(accessToken) { + if (!accessToken) return []; + if (!checkAccess(accessToken)) { + let new_access = await get_new_tokens(accessToken); + if (!new_access) { + localStorage.removeItem("accessToken"); + return []; + } + accessToken = new_access; + localStorage.setItem("accessToken", new_access); + } + let group = document.getElementById('groups').value; + try { + let response = await axios.get(api_url + "/api/get_files/" + group, { + headers: { + 'Authorization': 'Bearer ' + accessToken + } + }) + if (!response) return; let logim_page_btn = document.getElementById('login_page_a'); logim_page_btn.textContent = "Logout"; logim_page_btn.href = "/"; - logim_page_btn.onclick = function() {if (confirm("Log out?")) {logout()}}; + logim_page_btn.onclick = function () { if (confirm("Log out?")) { logout() } }; document.title = "File uploader · " + response.data.username; document.getElementById('login_mess').textContent = "Logged as " + response.data.username; + document.getElementById('invite_users').disabled = !response.data.is_group_owner; + document.getElementById('leave').title = response.data.is_group_owner ? "Delete group" : "Leave group"; + document.getElementById('leave').disabled = (response.data.is_group_owner == null); + document.getElementById('leave').onclick = response.data.is_group_owner ? () => {delete_group()} : () => {leave_group()} + + let len = 0; + let table = document.getElementById('files_table'); + table.innerHTML = ""; + if (group == "private") { + let file_history = JSON.parse(localStorage.getItem("file_history") || "[]"); + if (file_history != []) { + for (const file of file_history) { + append_to_files_arr(file, len); + len++; + } + } + // Insert a row at the start of table + let newRow = table.insertRow(0); + newRow.id = "transfer_row"; + // Insert a cell at the end of the row + let newCell = newRow.insertCell(); + // Append a text node to the cell + let transfer = document.createElement("button"); + transfer.id = "trensfer"; + transfer.innerHTML = "Transfer local files to an account" + transfer.onclick = function () { if (confirm("Transfer local files to an active account?")) transfer_func() } + if (len > 0) newCell.appendChild(transfer); + } let it = 0; - for (const file of response.data.data){ + for (const file of response.data.data) { append_to_files_arr(file, len + it); it++; } - }catch{ - localStorage.removeItem("accessToken"); + } catch (e) { + console.log(e); + if (e.response && e.response.status == 401) { + localStorage.removeItem("accessToken"); + return []; + } return []; } } -async function logout(){ +async function logout() { let accessToken = localStorage.getItem("accessToken"); - console.log(accessToken); if (!accessToken) return []; - if (!checkAccess(accessToken)){ + if (!checkAccess(accessToken)) { let new_access = await get_new_tokens(accessToken); - if (!new_access){ + if (!new_access) { localStorage.removeItem("accessToken"); return []; } - console.log(new_access); accessToken = new_access; localStorage.setItem("accessToken", new_access); } - try{ + try { let response = await axios.post(api_url + "/api/logout", {}, { headers: { 'Authorization': 'Bearer ' + accessToken } }) - if (!response) return; - if (response.status == 401 || response.status == 200){ + if (!response) return []; + if (response.status == 401 || response.status == 200) { localStorage.removeItem("accessToken"); let logim_page_btn = document.getElementById('login_page_a'); logim_page_btn.textContent = "Login"; logim_page_btn.href = "https://fu.andcool.ru/login/"; - } - }catch{ + } catch { localStorage.removeItem("accessToken"); return []; } } addEventListener("DOMContentLoaded", (event) => { - document.getElementById('input_file').addEventListener('change', function(e) { + document.getElementById('groups').addEventListener("change", (event) => { + localStorage.setItem("prev_group", event.target.value); + document.getElementById('invite_link_link').style.display = "none"; + fetch_files(localStorage.getItem("accessToken"), + event.target.value); + }); + document.getElementById('input_file').addEventListener('change', function (e) { let fileInput = document.getElementById('input_file'); let load_mess = document.getElementById('load_mess'); @@ -188,43 +466,42 @@ addEventListener("DOMContentLoaded", (event) => { if (file) { let reader = new FileReader(); - reader.onprogress = function(e) { - if (e.lengthComputable) { - let percentage = (e.loaded / e.total) * 100; - load_mess.textContent = "Uploading file... " + percentage.toFixed(1) + '%'; - } - }; + reader.onprogress = function (e) { + if (e.lengthComputable) { + let percentage = (e.loaded / e.total) * 100; + load_mess.textContent = "Uploading file... " + percentage.toFixed(1) + '%'; + } + }; - reader.onload = function() { - console.log('File loaded successfully'); - if (file.size > 100 * 1024 * 1024){ - load_mess.textContent = "File size exceeds the limit (100MB)"; - return; - } - upload(file); - }; + reader.onload = function () { + console.log('File loaded successfully'); + if (file.size > 100 * 1024 * 1024) { + load_mess.textContent = "File size exceeds the limit (100MB)"; + return; + } + upload(file); + }; - reader.readAsDataURL(file); + reader.readAsDataURL(file); } }); - + let len = 0; let file_history = JSON.parse(localStorage.getItem("file_history") || "[]"); - if (file_history != []){ - let it = 0; - for (const file of file_history){ - append_to_files_arr(file, it); - it++; + if (file_history != []) { + for (const file of file_history) { + append_to_files_arr(file, len); + len++; } } - fetch_files(localStorage.getItem("accessToken"), file_history.length); - + fetch_groups(); let dropContainer = document.getElementById('dropContainer') - dropContainer.ondragover = dropContainer.ondragenter = function(evt) { + dropContainer.ondragover = dropContainer.ondragenter = function (evt) { evt.preventDefault(); - }; - - dropContainer.ondrop = function(evt) { + }; + + + dropContainer.ondrop = function (evt) { const dT = new DataTransfer(); dT.items.add(evt.dataTransfer.files[0]); document.getElementById('input_file').files = dT.files; @@ -234,59 +511,64 @@ addEventListener("DOMContentLoaded", (event) => { }); -async function upload(file){ - - let imagefile = document.querySelector('#input_file'); - let load_mess = document.getElementById('load_mess'); +async function upload(file) { + + let imagefile = document.querySelector('#input_file'); + let load_mess = document.getElementById('load_mess'); let file_ext = document.getElementById('include_ext'); - load_mess.textContent = "Uploading to the server..."; - - try{ - let response = await axios.post(api_url + api_upload_url + "?include_ext=" + file_ext.checked, {'file': file}, { - headers: { - 'Content-Type': 'multipart/form-data', - 'Authorization': 'Bearer ' + localStorage.getItem("accessToken") - } - }) - - imagefile.value = ""; - if (response){ - if (response.status == 200){ - let response1 = response.data; - var currentdate = new Date(); + let group_id = document.getElementById('groups').value; + load_mess.textContent = "Uploading to the server..."; + + try { + let response = await axios.post(`${api_url}${api_upload_url}/${group_id}?include_ext=${file_ext.checked}`, { 'file': file }, { + headers: { + 'Content-Type': 'multipart/form-data', + 'Authorization': 'Bearer ' + localStorage.getItem("accessToken") + } + }) + + imagefile.value = ""; + if (response) { + if (response.status == 200) { + let response1 = response.data; + var currentdate = new Date(); var datetime = currentdate.getDate() + "." - + (currentdate.getMonth()+1) + "." - + currentdate.getFullYear() + " " - + currentdate.getHours() + ":" - + currentdate.getMinutes() + ":" - + currentdate.getSeconds(); - + + (currentdate.getMonth() + 1) + "." + + currentdate.getFullYear() + " " + + currentdate.getHours() + ":" + + currentdate.getMinutes() + ":" + + currentdate.getSeconds(); + response1["creation_date"] = datetime; load_mess.textContent = "Uploaded!"; - + let old_data = JSON.parse(localStorage.getItem("file_history") || "[]"); append_to_files_arr(response1, old_data.length) navigator.clipboard.writeText(response1['file_url_full']); - - if (!response1.synced){ + + if (!response1.synced) { old_data.push(response1); localStorage.setItem("file_history", JSON.stringify(old_data)); } - } - else if (response.status == 429){ - load_mess.textContent = "Too many requests (2 per minute)"; - }else{ - load_mess.textContent = response.data.message; - } - } - }catch(err){ - console.log(err); - load_mess.textContent = "Error while uploading file. See console for more information."; + } + } + } catch (err) { + console.log(err); + if(err.response){ + if(err.response.status == 429){ + load_mess.textContent = "Too many requests (2 per minute)"; + return; + }else{ + load_mess.textContent = err.response.data.message; + return + } + } + load_mess.textContent = "Error while uploading file. See console for more information."; imagefile.value = ""; - } - + } + } diff --git a/web/snowflakes.js b/web/snowflakes.js index 3cc756f..15df132 100644 --- a/web/snowflakes.js +++ b/web/snowflakes.js @@ -1,9 +1,9 @@ -function getRandomInt(min, max){ return Math.floor(Math.random() * (max - min + 1)) + min; } +function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } let object_array = []; -class Circle{ - constructor(object){ +class Circle { + constructor(object) { this.object = object; this.cnavas_width = 200; this.vw = window.innerWidth; @@ -22,21 +22,21 @@ class Circle{ this.last_scroll = document.documentElement.scrollTop || document.body.scrollTop; } - tick(){ + tick() { let now_time = Date.now(); const circle = this.object; this.posY += this.speedY; this.posX += Math.sin(now_time / 1000) * this.random_sin_mod; - if (this.posY > this.vh){ this.posY = getRandomInt(-this.vw, -50); } - if (this.posX > this.vw){ + if (this.posY > this.vh) { this.posY = getRandomInt(-this.vw, -50); } + if (this.posX > this.vw) { this.posX = -10; } - if (this.posX < -10){ this.posX = this.vw; } + if (this.posX < -10) { this.posX = this.vw; } this.posY -= (document.documentElement.scrollTop || document.body.scrollTop) - this.last_scroll; - if (now_time - this.last_time > this.random_time){ + if (now_time - this.last_time > this.random_time) { this.last_time = now_time; this.random_time = getRandomInt(1000, 5000); this.random_sin_mod = getRandomInt(1, 3); @@ -48,7 +48,7 @@ class Circle{ } } -function run_anim(){ +function run_anim() { let table = document.getElementById('canvas'); let vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); let count = 30 @@ -57,24 +57,24 @@ function run_anim(){ let day = date.getDate(); let month = date.getMonth() + 1; if (!((month == 12 && day > 20) || (month == 1 && day < 5)) || vw < 900) return; - - if( localStorage.getItem("disable_snow"))return; - + + if (localStorage.getItem("disable_snow")) return; + let snow_btn = document.getElementById('snow_btn'); snow_btn.style.display = "block"; table.style.display = "block"; - for (let x = 0; x < count; x++){ + for (let x = 0; x < count; x++) { var snowflake = document.createElement("p"); snowflake.innerHTML = "*"; snowflake.className = "snowflake"; table.appendChild(snowflake); object_array.push(new Circle(snowflake)); - + } } -function tick(){ - for (const snowflake of object_array){ +function tick() { + for (const snowflake of object_array) { snowflake.tick(); } } @@ -82,7 +82,7 @@ function tick(){ setInterval(tick, 16); -function stop_anim(){ +function stop_anim() { object_array = []; let table = document.getElementById('canvas'); table.innerHTML = ""; diff --git a/web/style.css b/web/style.css index ebd0a24..10c3db2 100644 --- a/web/style.css +++ b/web/style.css @@ -5,8 +5,8 @@ body{ justify-content: center; background-color: #222222; color: #eeeeee; - height: 100%; overflow-x: hidden; + margin: 0; } main{ @@ -67,7 +67,7 @@ p{margin-left: 2px;} #files_table tbody{width: 100%;} -#files_table tr{ +#files_table .tr{ display: flex; position: relative; outline: none; @@ -83,7 +83,7 @@ p{margin-left: 2px;} margin-bottom: 2%; } -#files_table button{ +#files_table .button{ cursor: pointer; background-color: #222222; color:#eeeeee; @@ -115,7 +115,8 @@ p{margin-left: 2px;} cursor: pointer; color: white; text-decoration: none; - word-wrap: break-word; + text-wrap: wrap; + display: block; } #files_table #url:hover{ @@ -247,6 +248,7 @@ nav a:hover{ display: flex; align-items: center; flex-direction: row; + text-overflow: ellipsis; } .href_btn{ @@ -255,6 +257,91 @@ nav a:hover{ justify-content: center; margin-right: 1%; } + +#transfer_row{ + border: none; +} + +#trensfer{ + margin-bottom: 3%; + margin-left: 0; + cursor: pointer; + background-color: #222222; + color:#eeeeee; + font-family: 'Roboto Mono', monospace; + font-weight: 400; + font-size: 80%; + border: 2px rgb(105, 105, 105) solid; + border-radius: 10px; + padding: 5px; + font-weight: 400; + transition: background-color 0.2s; +} + +#groups{ + background-color: #222222; + color: white; + border: 2px rgb(105, 105, 105) solid; + border-radius: 10px; + padding: 5px; + max-width: 100%; +} + +.group_btn{ + background-color: #222222; + color: #eeeeee; + border: 2px rgb(105, 105, 105) solid; + border-radius: 10px; + width: 31px; + height: 31px; + font-family: 'Roboto Mono', monospace; + font-weight: 600; + font-size: 0.9rem; + padding: 0px; + margin-left: 5px; + transition: background-color 0.2s; + cursor: pointer; +} + +.group_btn:hover{ + background-color: rgb(109, 109, 109); + transition: background-color 0.2s; +} + +#username{ + font-size: 70%; + color: rgb(200, 200, 200) +} + +.group_btn:disabled{ + cursor: default; + border: 2px rgb(68, 68, 68) solid; +} +.group_btn:disabled:hover{ + background-color: #222222; +} + +.button_creation_class{ + background-color: #222222; + border: 2px rgb(105, 105, 105) solid; + border-radius: 10px; + padding: 10px; + color: white; + cursor: pointer; + transition: background-color 0.2s; +} + +.button_creation_class:hover{ + background-color: rgb(109, 109, 109); + transition: background-color 0.2s; +} + +#external{ + width: 16px; + height: 16px; + +} + @media(max-width: 425px){ main{width: 90%;} h1{font-size: 130%;} @@ -264,12 +351,12 @@ nav a:hover{ padding: 7px; border-radius: 10px; } - #files_table tr{padding: 2%; flex-wrap: wrap;} + #files_table .tr{padding: 2%; flex-wrap: wrap;} #files_table p{font-size: 70%; margin: 0; word-break: break-all;} - #files_table button{font-size: 70%; padding: 7px; margin: 0} + #files_table .button{font-size: 70%; padding: 7px; margin: 0} #files_table #url{font-size: 70%;} #addit_sett{padding-left: 2%; margin-bottom: 3%;} #addit_sett_btn{margin-bottom: 3%;} .add_label{font-size: 80%;} - #files_table tr {flex-direction: column; align-items: flex-start;} + #files_table .tr {flex-direction: column; align-items: flex-start;} } \ No newline at end of file