From 70cab7eaf25ceb124d186fc47038f21f7f108ba0 Mon Sep 17 00:00:00 2001 From: AndcoolSystems Date: Sun, 4 Feb 2024 00:00:47 +0300 Subject: [PATCH 01/10] added groups api (not full) --- .gitignore | 3 +- README.md | 20 +-- imports.py | 23 ++++ main.py | 270 ++++++++++++++++++++++++++++++++++---- schema.prisma | 19 ++- web/{script.js => api.js} | 119 ++++++++++++++++- web/index.html | 11 +- web/style.css | 30 ++++- 8 files changed, 439 insertions(+), 56 deletions(-) create mode 100644 imports.py rename web/{script.js => api.js} (73%) diff --git a/.gitignore b/.gitignore index aad3bbc..bc22080 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /__pycache__ /dataBase.db /.env -/tg \ No newline at end of file +/tg +/web_nextjs diff --git a/README.md b/README.md index a221a6b..18ea244 100644 --- a/README.md +++ b/README.md @@ -39,13 +39,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,8 +56,8 @@ If the file type cannot be determined, the API returns the file in download mode #### Possible Errors -| Error Code | Description | Possible Reasons | -| ---------- | ----------------------------- | ------------------------------------------ | +| Error Code | Description | Possible Reasons | +| ---------- | ----------------------------- | -----------------------------------------------| | 404 | File not found | The file referenced by the code does not exist | ### Upload a file to the server @@ -122,7 +122,7 @@ Both requests accept the same request body but have different errors. ```json { - "username": "Andcool", + "username": "My cool username", "password": "My cool password" } ``` @@ -168,7 +168,7 @@ 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 | Errors described in section `1.1` may also occur. diff --git a/imports.py b/imports.py new file mode 100644 index 0000000..a8e4f6a --- /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 \ No newline at end of file diff --git a/main.py b/main.py index 3420c3f..39aff04 100644 --- a/main.py +++ b/main.py @@ -2,26 +2,7 @@ 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'): @@ -46,7 +27,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, @@ -62,7 +43,7 @@ async def startup_event(): 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", @@ -82,7 +63,7 @@ async def check_token(Authorization): 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} @@ -110,8 +91,8 @@ async def upload_file(file: UploadFile, request: Request, include_ext: bool = Fa if max_uses > 10000: return JSONResponse(content={"status": "error", "message": "Invalid max_uses parameter"}, status_code=400) - user_id = -1 saved_to_account = False + user_id = -1 token_db, auth_error = await check_token(Authorization) # Check token if token_db: # If token is okay @@ -212,18 +193,19 @@ 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 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) 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 files_response = [] for file in files: + user_filename = file.user_filename[:50] + ("..." if len(file.user_filename) > 50 else "") 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, + "user_filename": user_filename, "creation_date": file.created_date, "craeted_at": file.craeted_at, "size": utils.calculate_size(file.size), @@ -310,7 +292,7 @@ async def login(request: Request): 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) + 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 @@ -329,7 +311,7 @@ async def login(request: Request, 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 @@ -337,5 +319,237 @@ async def login(request: Request, "message": "logged out"} +@app.post("/api/transfer") # logout 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) + + group = await db.group.create(data={ + "name": body["group_name"], + "group_id": random.randint(10000000, 99999999), + "admin_id": token_db.user_id, + "invite_string": utils.generate_token(15), + 'members': { + 'connect': { + 'id': token_db.user_id + }, + } + }) + return {"status": "success", + "message": "created", + "name": group.name, + "invite_string": group.invite_string, + "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.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) + + group = await db.group.find_first(where={"invite_string": invite_link}, include={"members": True}) + if not group: + return JSONResponse({"status": "error", "message": "Invite link not found"}, status_code=404) + + 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} + ) + + return {"status": "success", + "message": "Joined", + "name": group.name, + "invite_string": group.invite_string, + "group_id": 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.post("/api/group/{group_id}/upload") # leave handler +@limiter.limit(dynamic_limit_provider) +async def upload_group(group_id: int, file: UploadFile, request: Request, include_ext: bool = False, max_uses: int = 0, + 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) + + if not file: # Check, if the file is uploaded + return JSONResponse(content={"status": "error", "message": "No file uploaded"}, status_code=400) + + 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 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) + + key = str(uuid.uuid4()) # Generate unique delete key + ext = "." + file.filename.split(".")[-1].lower() # Get file extension + fid = utils.generate_token(10) + (ext if include_ext else "") # Generate file url + 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": group.group_id * -1, + "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": False, + "auth_error": auth_error}, status_code=200) + + +@app.get("/api/get_groups") # leave 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, + "invite_string": group.invite_string + }) + + return {"status": "success", + "message": "groups got successfully", + "groups": groups} + + if __name__ == "__main__": # Start program uvicorn.run("main:app", reload=True, port=8080) diff --git a/schema.prisma b/schema.prisma index c943748..61c52ad 100644 --- a/schema.prisma +++ b/schema.prisma @@ -32,11 +32,22 @@ 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[] + invite_string String @default("") +} + diff --git a/web/script.js b/web/api.js similarity index 73% rename from web/script.js rename to web/api.js index f9126f3..680b9cf 100644 --- a/web/script.js +++ b/web/api.js @@ -1,7 +1,8 @@ let api_upload_url = "/api/upload"; let api_file_url = "/file/"; -let api_url = "https://fu.andcool.ru"; -//let api_url = "http://127.0.0.1:8080"; +//let api_url = "https://fu.andcool.ru"; +let api_url = "http://127.0.0.1:8080"; +let groups = []; async function delete_file(data, id){ let confirmed = confirm("Delete it? It will be impossible to restore the file!"); @@ -25,6 +26,7 @@ function append_to_files_arr(data, id){ // Insert a row at the start of table let newRow = table.insertRow(0); newRow.id = "row_" + id; + newRow.className = "tr"; // Insert a cell at the end of the row let newCell = newRow.insertCell(); @@ -50,6 +52,7 @@ function append_to_files_arr(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"); @@ -90,6 +93,7 @@ function append_to_files_arr(data, id){ newCell2.appendChild(button); } + async function get_new_tokens(accessToken){ try{ let response = await axios.post(api_url + "/api/refresh_token", {'accessToken': "Bearer " + accessToken}, {}) @@ -101,7 +105,9 @@ async function get_new_tokens(accessToken){ } } -async function fetch_files(accessToken, len){ + +async function transfer_func(){ + accessToken = localStorage.getItem("accessToken"); if (!accessToken) return []; if (!checkAccess(accessToken)){ let new_access = await get_new_tokens(accessToken); @@ -114,16 +120,95 @@ async function fetch_files(accessToken, len){ localStorage.setItem("accessToken", new_access); } try{ - let response = await axios.get(api_url + "/api/get_files", { + 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){ + console.log(e); + if (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 []; + } + console.log(new_access); + 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; + + /*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()}}; + document.title = "File uploader · " + response.data.username; + + document.getElementById('login_mess').textContent = "Logged as " + response.data.username;*/ + + let groups = document.getElementById('groups'); + + for (const group of response.data.groups){ + let groupel = document.createElement("option"); + groupel.innerHTML = group.name; + groupel.value = group.group_id; + groups.appendChild(groupel); + } + + }catch (e){ + console.log(e); + if (e.response.status == 401){ + localStorage.removeItem("accessToken"); + return []; + } + return []; + } +} + +async function fetch_files(accessToken, len){ + if (!accessToken) return []; + if (!checkAccess(accessToken)){ + let new_access = await get_new_tokens(accessToken); + 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", { + headers: { + 'Authorization': 'Bearer ' + accessToken + } + }) + if (!response) return; let logim_page_btn = document.getElementById('login_page_a'); logim_page_btn.textContent = "Logout"; @@ -132,14 +217,33 @@ async function fetch_files(accessToken, len){ document.title = "File uploader · " + response.data.username; document.getElementById('login_mess').textContent = "Logged as " + response.data.username; + + let table = document.getElementById('files_table'); + + // 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){ append_to_files_arr(file, len + it); it++; } - }catch{ - localStorage.removeItem("accessToken"); + }catch (e){ + console.log(e); + if (e.response.status == 401){ + localStorage.removeItem("accessToken"); + return []; + } return []; } } @@ -216,6 +320,7 @@ addEventListener("DOMContentLoaded", (event) => { it++; } } + fetch_groups(); fetch_files(localStorage.getItem("accessToken"), file_history.length); diff --git a/web/index.html b/web/index.html index f4ebb4b..05dc8b1 100644 --- a/web/index.html +++ b/web/index.html @@ -9,7 +9,7 @@ - +
@@ -72,6 +75,12 @@

File uploader

Choose file + +

Select file group:

+

diff --git a/web/style.css b/web/style.css index ebd0a24..f7d9804 100644 --- a/web/style.css +++ b/web/style.css @@ -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; @@ -255,6 +255,26 @@ 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; +} @media(max-width: 425px){ main{width: 90%;} h1{font-size: 130%;} @@ -264,12 +284,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 From b49168f2de21d7fa8220cde3073d0142a1d1e1fe Mon Sep 17 00:00:00 2001 From: AndcoolSystems Date: Sun, 4 Feb 2024 01:40:37 +0300 Subject: [PATCH 02/10] added group selector on site --- main.py | 26 ++++++++++++++---- web/api.js | 72 ++++++++++++++++++++++---------------------------- web/index.html | 5 +++- web/style.css | 13 ++++++++- 4 files changed, 69 insertions(+), 47 deletions(-) diff --git a/main.py b/main.py index 39aff04..14a1878 100644 --- a/main.py +++ b/main.py @@ -186,17 +186,31 @@ async def delete_file(url: str, key: str = ""): 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, +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", "message": "Auth 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 + user = await db.user.find_first(where={"id": token_db.user_id}) # Get user files from db + + if group_id == "private": + user_id = user.id + 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) + + user_id = -int(group_id) + files = await db.file.find_many(where={"user_id": user_id}) # Get all user files from db files_response = [] for file in files: user_filename = file.user_filename[:50] + ("..." if len(file.user_filename) > 50 else "") @@ -368,6 +382,8 @@ async def create_group(request: Request, 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), diff --git a/web/api.js b/web/api.js index 680b9cf..0196f15 100644 --- a/web/api.js +++ b/web/api.js @@ -32,12 +32,10 @@ function append_to_files_arr(data, id){ let newCell = newRow.insertCell(); let newCell2 = newRow.insertCell(); // Append a text node to the cell - let url = document.createElement("a"); + 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"; let filename = document.createElement("p"); filename.innerHTML = data['user_filename']; @@ -84,7 +82,7 @@ function append_to_files_arr(data, id){ urls_div.appendChild(url_link_div); urls_div.appendChild(filename); - + creation_date_div.appendChild(cr_time); creation_date_div.appendChild(online); urls_div.appendChild(creation_date_div); @@ -163,16 +161,7 @@ async function fetch_groups(){ }) 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()}}; - document.title = "File uploader · " + response.data.username; - - document.getElementById('login_mess').textContent = "Logged as " + response.data.username;*/ - let groups = document.getElementById('groups'); - for (const group of response.data.groups){ let groupel = document.createElement("option"); groupel.innerHTML = group.name; @@ -190,7 +179,7 @@ async function fetch_groups(){ } } -async function fetch_files(accessToken, len){ +async function fetch_files(accessToken, group){ if (!accessToken) return []; if (!checkAccess(accessToken)){ let new_access = await get_new_tokens(accessToken); @@ -203,7 +192,7 @@ async function fetch_files(accessToken, len){ localStorage.setItem("accessToken", new_access); } try{ - let response = await axios.get(api_url + "/api/get_files", { + let response = await axios.get(api_url + "/api/get_files/" + group, { headers: { 'Authorization': 'Bearer ' + accessToken } @@ -218,20 +207,29 @@ async function fetch_files(accessToken, len){ document.getElementById('login_mess').textContent = "Logged as " + response.data.username; + let len = 0; let table = document.getElementById('files_table'); - - // 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); - + 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){ append_to_files_arr(file, len + it); @@ -268,14 +266,13 @@ async function logout(){ 'Authorization': 'Bearer ' + accessToken } }) - if (!response) return; + 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{ localStorage.removeItem("accessToken"); @@ -284,6 +281,10 @@ async function logout(){ } addEventListener("DOMContentLoaded", (event) => { + document.getElementById('groups').addEventListener("change", (event) => { + 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'); @@ -312,17 +313,8 @@ addEventListener("DOMContentLoaded", (event) => { } }); - 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++; - } - } fetch_groups(); - fetch_files(localStorage.getItem("accessToken"), file_history.length); - + fetch_files(localStorage.getItem("accessToken"), document.getElementById('groups').value); let dropContainer = document.getElementById('dropContainer') dropContainer.ondragover = dropContainer.ondragenter = function(evt) { diff --git a/web/index.html b/web/index.html index 05dc8b1..7a08adf 100644 --- a/web/index.html +++ b/web/index.html @@ -7,6 +7,9 @@ + + + @@ -77,7 +80,7 @@

File uploader

Select file group:

- diff --git a/web/style.css b/web/style.css index f7d9804..1b3da62 100644 --- a/web/style.css +++ b/web/style.css @@ -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{ @@ -275,6 +277,15 @@ nav a:hover{ 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%; +} @media(max-width: 425px){ main{width: 90%;} h1{font-size: 130%;} From 0a5bf6c3dbbb50754d92c4dfdac4f63cd032ac85 Mon Sep 17 00:00:00 2001 From: AndcoolSystems Date: Sun, 4 Feb 2024 14:51:58 +0300 Subject: [PATCH 03/10] updated upload api, added creation and invite buttons --- imports.py | 2 +- main.py | 927 +++++++++++++++++++++++++++--------------- schema.prisma | 1 + utils.py | 3 +- web/api.js | 296 +++++++------- web/index.html | 16 +- web/res/user_plus.svg | 1 + web/snowflakes.js | 34 +- web/style.css | 37 +- 9 files changed, 829 insertions(+), 488 deletions(-) create mode 100644 web/res/user_plus.svg diff --git a/imports.py b/imports.py index a8e4f6a..fc02171 100644 --- a/imports.py +++ b/imports.py @@ -20,4 +20,4 @@ import bcrypt import random -rate_limit_exceeded_handler = _rate_limit_exceeded_handler \ No newline at end of file +rate_limit_exceeded_handler = _rate_limit_exceeded_handler diff --git a/main.py b/main.py index 14a1878..4894e4f 100644 --- a/main.py +++ b/main.py @@ -4,8 +4,9 @@ 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" @@ -37,104 +38,181 @@ 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") # root api endpoint +@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]}, include={"user": True}) # 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.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): +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 not file: # Check, if the file is uploaded - return JSONResponse(content={"status": "error", "message": "No file uploaded"}, status_code=400) + return JSONResponse( + content={"status": "error", "message": "No file uploaded"}, status_code=400 + ) if file.filename.find(".") == -1: # Check, if the file has a extension - return JSONResponse(content={"status": "error", "message": "Bad file extension"}, status_code=400) + return JSONResponse( + content={"status": "error", "message": "Bad file extension"}, + 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) - + 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=400) + else: + group_id = -1 key = str(uuid.uuid4()) # Generate unique delete key ext = "." + file.filename.split(".")[-1].lower() # 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.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 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 @@ -142,159 +220,288 @@ 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/{group_id}") # get files handler @app.get("/api/get_files/{group_id}") # get files handler @limiter.limit(dynamic_limit_provider) -async def getFiles(group_id: str, 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", "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 + 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": - user_id = user.id + files = await db.file.find_many(where={"user_id": user.id} + ) # 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}) + 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) + 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) - - user_id = -int(group_id) - files = await db.file.find_many(where={"user_id": user_id}) # Get all user files from db + 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: - user_filename = file.user_filename[:50] + ("..." if len(file.user_filename) > 50 else "") - 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), - "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 "" + ) + 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": token_db.user.username if group_id != "private" else None, + "synced": True, + } + ) + return JSONResponse( + content={ + "status": "success", + "message": "messages got successfully", + "username": user.username, + "is_group_owner": False 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 @@ -302,54 +509,95 @@ async def login(request: Request, bot: bool = False): async def login(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", "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}, # 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 login( + 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) + 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") # logout handler @limiter.limit(dynamic_limit_provider) -async def transfer(request: Request, - Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None): - +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) - + 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) - + 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) - + return JSONResponse( + content={"status": "error", "message": "No `data` field in request body"}, + status_code=400, + ) + non_success = [] for requested_file in body["data"]: try: @@ -358,213 +606,242 @@ async def transfer(request: Request, non_success.append(requested_file) continue - await db.file.update(where={"id": file.id}, data={"user_id": token_db.user_id}) + 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} + 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): - +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) - + 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) - + 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, - "invite_string": utils.generate_token(15), - 'members': { - 'connect': { - 'id': token_db.user_id - }, - } - }) - return {"status": "success", - "message": "created", - "name": group.name, - "invite_string": group.invite_string, - "group_id": group.group_id} + 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, + "invite_string": utils.generate_token(15), + "members": { + "connect": {"id": token_db.user_id}, + }, + } + ) + return { + "status": "success", + "message": "created", + "name": group.name, + "invite_string": group.invite_string, + "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): - +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) - + 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) - + 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) - + 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"} + return {"status": "success", "message": "deleted"} @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): +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) - - group = await db.group.find_first(where={"invite_string": invite_link}, include={"members": True}) + return JSONResponse( + content={ + "status": "error", + "message": "Auth error", + "auth_error": auth_error, + }, + status_code=401, + ) + + group = await db.group.find_first( + where={"invite_string": invite_link}, include={"members": True} + ) if not group: - return JSONResponse({"status": "error", "message": "Invite link not found"}, status_code=404) - + return JSONResponse( + {"status": "error", "message": "Invite link not found"}, status_code=404 + ) + 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} - ) - - return {"status": "success", - "message": "Joined", - "name": group.name, - "invite_string": group.invite_string, - "group_id": group.group_id} + 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}, + ) + + return { + "status": "success", + "message": "Joined", + "name": group.name, + "invite_string": group.invite_string, + "group_id": 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): +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.post("/api/group/{group_id}/upload") # leave handler -@limiter.limit(dynamic_limit_provider) -async def upload_group(group_id: int, file: UploadFile, request: Request, include_ext: bool = False, max_uses: int = 0, - Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None): + return JSONResponse( + content={ + "status": "error", + "message": "Auth error", + "auth_error": auth_error, + }, + status_code=401, + ) - 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}) + 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) - - if not file: # Check, if the file is uploaded - return JSONResponse(content={"status": "error", "message": "No file uploaded"}, status_code=400) + return JSONResponse( + {"status": "error", "message": "Group not found"}, status_code=404 + ) - 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 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) - - key = str(uuid.uuid4()) # Generate unique delete key - ext = "." + file.filename.split(".")[-1].lower() # Get file extension - fid = utils.generate_token(10) + (ext if include_ext else "") # Generate file url - fn = str(uuid.uuid4()) + ext # Generate file name + if token_db.user not in group.members: + return JSONResponse( + {"status": "error", "message": "You are not in the group"}, status_code=400 + ) - async with aiofiles.open(f"uploads/{fn}", "wb") as f: # Save file locally - await f.write(file.file.read()) + await db.group.update( + data={"members": {"disconnect": {"id": token_db.user_id}}}, + where={"id": group.id}, + ) - now = datetime.now() - created = await db.file.create({ # Creating a file record - "user_id": group.group_id * -1, - "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": False, - "auth_error": auth_error}, status_code=200) + return {"status": "success", "message": "leaved"} @app.get("/api/get_groups") # leave handler @limiter.limit(dynamic_limit_provider) -async def get_groups(request: Request, Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None): - +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}) + 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, - "invite_string": group.invite_string - }) - - return {"status": "success", - "message": "groups got successfully", - "groups": groups} + groups.append( + { + "name": group.name, + "group_id": group.group_id, + "invite_string": group.invite_string, + } + ) + + return {"status": "success", "message": "groups got successfully", "groups": groups} if __name__ == "__main__": # Start program diff --git a/schema.prisma b/schema.prisma index 61c52ad..dfe15b3 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("") 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/api.js b/web/api.js index 0196f15..2938f15 100644 --- a/web/api.js +++ b/web/api.js @@ -2,14 +2,13 @@ let api_upload_url = "/api/upload"; let api_file_url = "/file/"; //let api_url = "https://fu.andcool.ru"; let api_url = "http://127.0.0.1:8080"; -let groups = []; -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)); @@ -20,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; 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']; + // 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.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'; + const button = document.createElement('button'); + button.innerHTML = 'Delete'; button.className = "button" - button.onclick = function(){delete_file(data, id);} + 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"; } @@ -71,9 +70,12 @@ function append_to_files_arr(data, id){ let href_img = document.createElement("img"); href_img.src = "./res/external-link.png"; - 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"; @@ -82,57 +84,56 @@ function append_to_files_arr(data, id){ urls_div.appendChild(url_link_div); urls_div.appendChild(filename); - + 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 transfer_func(){ +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.post(api_url + "/api/transfer", {'data': JSON.parse(localStorage.getItem("file_history") || "[]")}, { + 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 == 200){ + if (response.status == 200) { localStorage.setItem("file_history", JSON.stringify(response.data.unsuccess)); location.reload(); } - - }catch (e){ - console.log(e); - if (e.response.status == 401){ + + } catch (e) { + if (e.response && e.response.status == 401) { localStorage.removeItem("accessToken"); return []; } @@ -140,20 +141,19 @@ async function transfer_func(){ } } -async function fetch_groups(){ +async function fetch_groups() { let 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{ + try { let response = await axios.get(api_url + "/api/get_groups", { headers: { 'Authorization': 'Bearer ' + accessToken @@ -161,17 +161,25 @@ async function fetch_groups(){ }) if (!response) return; + document.getElementById('groups_selector').style.display = "block"; + let prev_group = localStorage.getItem("prev_group"); let groups = document.getElementById('groups'); - for (const group of response.data.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){ - console.log(e); - if (e.response.status == 401){ + } catch (e) { + if (e.response && e.response.status == 401) { localStorage.removeItem("accessToken"); return []; } @@ -179,19 +187,19 @@ async function fetch_groups(){ } } -async function fetch_files(accessToken, group){ +async function fetch_files(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 group = document.getElementById('groups').value; + try { let response = await axios.get(api_url + "/api/get_files/" + group, { headers: { 'Authorization': 'Bearer ' + accessToken @@ -202,18 +210,19 @@ async function fetch_files(accessToken, group){ 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; let len = 0; let table = document.getElementById('files_table'); - table.innerHTML = ""; - if (group == "private"){ + table.innerHTML = ""; + if (group == "private") { let file_history = JSON.parse(localStorage.getItem("file_history") || "[]"); - if (file_history != []){ - for (const file of file_history){ + if (file_history != []) { + for (const file of file_history) { append_to_files_arr(file, len); len++; } @@ -227,18 +236,18 @@ async function fetch_files(accessToken, group){ 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()} + 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 (e){ + } catch (e) { console.log(e); - if (e.response.status == 401){ + if (e.response && e.response.status == 401) { localStorage.removeItem("accessToken"); return []; } @@ -246,35 +255,33 @@ async function fetch_files(accessToken, group){ } } -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.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 []; } @@ -282,10 +289,11 @@ async function logout(){ addEventListener("DOMContentLoaded", (event) => { document.getElementById('groups').addEventListener("change", (event) => { - fetch_files(localStorage.getItem("accessToken"), - event.target.value); - }); - document.getElementById('input_file').addEventListener('change', function(e) { + localStorage.setItem("prev_group", event.target.value); + 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'); @@ -293,35 +301,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 != []) { + for (const file of file_history) { + append_to_files_arr(file, len); + len++; + } + } fetch_groups(); - fetch_files(localStorage.getItem("accessToken"), document.getElementById('groups').value); 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; @@ -331,59 +346,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/index.html b/web/index.html index 7a08adf..673e10b 100644 --- a/web/index.html +++ b/web/index.html @@ -79,11 +79,17 @@

File uploader

-

Select file group:

- +

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/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 1b3da62..4e60040 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{ @@ -286,6 +286,41 @@ nav a:hover{ 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) +} + +#invite_users:disabled{ + cursor: default; + border: 2px rgb(68, 68, 68) solid; +} +#invite_users:disabled:hover{ + background-color: #222222; +} + @media(max-width: 425px){ main{width: 90%;} h1{font-size: 130%;} From e02b11ade56cd0a0786856132996518381f46154 Mon Sep 17 00:00:00 2001 From: AndcoolSystems Date: Sun, 4 Feb 2024 17:59:14 +0300 Subject: [PATCH 04/10] pre-release --- accept_invite.html | 213 ++++++++++++++++++++++++++++++++++++++ main.py | 111 +++++++++++++++++--- schema.prisma | 10 +- web/index.html | 20 +++- web/{api.js => script.js} | 168 +++++++++++++++++++++++++++++- web/style.css | 19 +++- 6 files changed, 520 insertions(+), 21 deletions(-) create mode 100644 accept_invite.html rename web/{api.js => script.js} (72%) 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/main.py b/main.py index 4894e4f..e363a67 100644 --- a/main.py +++ b/main.py @@ -90,6 +90,12 @@ async def check_token(Authorization): return token_db, {} +@app.get("/invite/{group_id}") # File upload 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( @@ -348,6 +354,7 @@ async def getFiles( user_filename = file.user_filename[:50] + ( "..." if len(file.user_filename) > 50 else "" ) + usr = await db.user.find_first(where={"id": file.user_id}) files_response.append( { "file_url": file.url, @@ -358,7 +365,7 @@ async def getFiles( "creation_date": file.created_date, "craeted_at": file.craeted_at, "size": utils.calculate_size(file.size), - "username": token_db.user.username if group_id != "private" else None, + "username": (usr.username if usr else None) if group_id != "private" else None, "synced": True, } ) @@ -367,7 +374,7 @@ async def getFiles( "status": "success", "message": "messages got successfully", "username": user.username, - "is_group_owner": False if group_id == "private" else group.admin_id == token_db.user_id, + "is_group_owner": None if group_id == "private" else group.admin_id == token_db.user_id, "data": files_response, }, status_code=200, @@ -506,7 +513,7 @@ async def login(request: Request, bot: bool = False): @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( @@ -541,7 +548,7 @@ async def login(request: Request): @app.post("/api/logout") # logout handler @limiter.limit(dynamic_limit_provider) -async def login( +async def logout( request: Request, Authorization: Annotated[ Union[str, None], Header(convert_underscores=False) @@ -657,7 +664,6 @@ async def create_group( "name": body["group_name"], "group_id": random.randint(10000000, 99999999), "admin_id": token_db.user_id, - "invite_string": utils.generate_token(15), "members": { "connect": {"id": token_db.user_id}, }, @@ -667,7 +673,6 @@ async def create_group( "status": "success", "message": "created", "name": group.name, - "invite_string": group.invite_string, "group_id": group.group_id, } @@ -713,6 +718,50 @@ async def delete_group( 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( @@ -734,14 +783,15 @@ async def delete_group( status_code=401, ) - group = await db.group.find_first( - where={"invite_string": invite_link}, include={"members": True} + invite = await db.invitements.find_first( + where={"data": invite_link}, include={"group": True} ) - if not group: + 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"}, @@ -757,15 +807,54 @@ async def delete_group( where={"id": group.id}, ) + await db.invitements.delete(where={"id": invite.id}) + return { "status": "success", "message": "Joined", "name": group.name, - "invite_string": group.invite_string, "group_id": group.group_id, } +@app.get("/api/invite_info/{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 + ) + + + 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( @@ -837,12 +926,10 @@ async def get_groups( { "name": group.name, "group_id": group.group_id, - "invite_string": group.invite_string, } ) return {"status": "success", "message": "groups got successfully", "groups": groups} - if __name__ == "__main__": # Start program uvicorn.run("main:app", reload=True, port=8080) diff --git a/schema.prisma b/schema.prisma index dfe15b3..9a22ff1 100644 --- a/schema.prisma +++ b/schema.prisma @@ -49,6 +49,14 @@ model Group{ group_id BigInt @default(0) admin_id Int @default(0) members User[] - invite_string String @default("") + 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/web/index.html b/web/index.html index 673e10b..20502bb 100644 --- a/web/index.html +++ b/web/index.html @@ -12,7 +12,7 @@ - + @@ -94,7 +94,7 @@

File uploader