From a2139cea9c838c84f43c8d4722c900b2194af58c Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Sat, 8 Jun 2024 23:43:58 +0200 Subject: [PATCH 01/22] sending password works --- backend/app/files/router.py | 13 +++++++------ backend/app/files/schemas.py | 4 ---- backend/app/files/service.py | 5 +++-- frontend/src/routes/user/home/+page.svelte | 6 +++++- frontend/src/server/files.ts | 6 ++++-- sonar-project.properties | 1 - 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/backend/app/files/router.py b/backend/app/files/router.py index 12ede61..028e0e7 100644 --- a/backend/app/files/router.py +++ b/backend/app/files/router.py @@ -1,6 +1,6 @@ from typing import Annotated -from fastapi import APIRouter, Depends, UploadFile +from fastapi import APIRouter, Depends, File, Form, UploadFile from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession @@ -9,12 +9,12 @@ from ..database import get_async_session from ..schemas import RequestStatus from .constants import FILE_PATH -from .schemas import FileUploaded, IsShared, MetadataFileResponse +from .schemas import FileUploaded, MetadataFileResponse from .service import ( delete_file, get_all_files_user, get_metadata_path, - upload_file_unencrypted, + upload_file, verify_file, verify_file_link, ) @@ -26,10 +26,11 @@ async def create_upload_file( current_user: Annotated[User, Depends(get_current_user)], session: Annotated[AsyncSession, Depends(get_async_session)], - file: UploadFile, - is_shared: IsShared, + file: UploadFile = File(...), # noqa: B008 + is_shared: bool = Form(...), + password: str = Form(...), ) -> FileUploaded: - new_file = await upload_file_unencrypted(session, file, current_user, is_shared.is_shared) + new_file = await upload_file(session, file, current_user, is_shared, password) return FileUploaded(filename=new_file, username=str(current_user.username)) diff --git a/backend/app/files/schemas.py b/backend/app/files/schemas.py index 665aad4..ddc30d2 100644 --- a/backend/app/files/schemas.py +++ b/backend/app/files/schemas.py @@ -9,10 +9,6 @@ class FileUploaded(BaseModel): username: str -class IsShared(BaseModel): - is_shared: bool = False - - class MetadataFileResponse(BaseModel): id: uuid.UUID name: str diff --git a/backend/app/files/service.py b/backend/app/files/service.py index 810bcff..8dd835b 100644 --- a/backend/app/files/service.py +++ b/backend/app/files/service.py @@ -15,11 +15,12 @@ from .schemas import MetadataFileResponse -async def upload_file_unencrypted( +async def upload_file( session: AsyncSession, file: UploadFile, current_user: Annotated[User, Depends(get_current_user)], is_shared: bool = False, + password: str | None = None, ) -> str: if not current_user.has_remaining_quota(): raise quota_exception @@ -35,7 +36,7 @@ async def upload_file_unencrypted( file_db = File( name=file.filename, path=file_path, - encrypted=False, + encrypted=password is not None, size=file.size, timestamp=datetime.now(), expiration=datetime.now() + timedelta(days=14), diff --git a/frontend/src/routes/user/home/+page.svelte b/frontend/src/routes/user/home/+page.svelte index d10a7e8..080a47c 100644 --- a/frontend/src/routes/user/home/+page.svelte +++ b/frontend/src/routes/user/home/+page.svelte @@ -31,7 +31,8 @@ } try { - await sendFileForSpecifiedUser(accessToken, filesToUpload[0]); + await sendFileForSpecifiedUser(accessToken, filesToUpload[0], uploadFilePassword); + uploadFilePassword = '' window.location.reload(); } catch (error) { if (!isAxiosError(error)) { @@ -70,6 +71,8 @@ $: ({ classes, getStyles } = useStyles()); let userFiles: FileMetadata[] = []; + let uploadFilePassword = ''; + let visible = false; onMount(async () => { @@ -124,6 +127,7 @@ Upload File + + + + + + +{/if} diff --git a/frontend/src/server/files.ts b/frontend/src/server/files.ts index 4765c0a..1641889 100644 --- a/frontend/src/server/files.ts +++ b/frontend/src/server/files.ts @@ -35,11 +35,12 @@ export const sendFileForSpecifiedUser = async (accessToken: string, file: File, return result.data; }; -export const getFileByPath = async (accessToken: string, path: string) => { +export const getFileByPath = async (accessToken: string, path: string, password: string) => { const result = await axios.get(`${BASE_URL}/files/download/${path}/`, { responseType: 'blob', headers: { - authorization: `Bearer ${accessToken}` + authorization: `Bearer ${accessToken}`, + "File-Password": password } }); From 6316b0d66f5fb5705b6a24f987f5a23f7d644c62 Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Sun, 9 Jun 2024 01:32:54 +0200 Subject: [PATCH 04/22] cors for correct psw forbidden for wrong --- backend/app/files/router.py | 13 +++++++++---- backend/app/files/service.py | 11 +++++++---- backend/app/main.py | 1 + frontend/src/server/files.ts | 8 +++++--- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/backend/app/files/router.py b/backend/app/files/router.py index bf35099..a294dd6 100644 --- a/backend/app/files/router.py +++ b/backend/app/files/router.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import Annotated from fastapi import APIRouter, Depends, File, Form, Header, UploadFile @@ -42,14 +43,18 @@ async def get_all_files( return await get_all_files_user(current_user, session) -@router.get("/download/{path}") +@router.post("/download/{path}") async def get_file( path: str, current_user: Annotated[User, Depends(get_current_user)], session: Annotated[AsyncSession, Depends(get_async_session)], + file_password: str = Form(...), ) -> FileResponse: + print(f"Path {path}") filename = await verify_file(path, current_user, session) - return FileResponse(FILE_PATH + path, filename=filename) + print(f"Filename: {filename}") + full_path = decrypt_file(path, file_password, current_user) + return FileResponse(full_path, filename=filename) @router.get("/download-link/{path}") @@ -57,11 +62,11 @@ async def get_file_link( path: str, session: Annotated[AsyncSession, Depends(get_async_session)], current_user: Annotated[User, Depends(get_current_user)], - file_password: str = Header(...), + file_password: Annotated[str | None, Header()], ) -> FileResponse: filename = await verify_file_link(path, session, current_user, file_password) full_path = decrypt_file(path, file_password, current_user) - return FileResponse(full_path, filename=filename) + return FileResponse(Path(FILE_PATH) / full_path, filename=filename) @router.get("/metadata/{path}", response_model=MetadataFileResponse) diff --git a/backend/app/files/service.py b/backend/app/files/service.py index fd2cffe..e8715ca 100644 --- a/backend/app/files/service.py +++ b/backend/app/files/service.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Annotated -from cryptography.fernet import Fernet +from cryptography.fernet import Fernet, InvalidToken from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC @@ -172,7 +172,7 @@ async def get_file_by_id(file_id: uuid.UUID, session: AsyncSession) -> File | No return file.scalar_one_or_none() -def decrypt_file(path: str, password: str, current_user: User) -> str: +def decrypt_file(path: str, password: str | None, current_user: User) -> str: if password is None or password == "": return (Path(FILE_PATH) / path).name @@ -182,14 +182,17 @@ def decrypt_file(path: str, password: str, current_user: User) -> str: with Path.open(Path(FILE_PATH) / path, "rb") as f: contents = f.read() - decoded_file = fernet.decrypt(contents) + try: + decoded_file = fernet.decrypt(contents) + except InvalidToken as err: + raise no_access_exception from err decrypted_file_tmp_path = Path(FILE_PATH) / f"{uuid.uuid4()}{path}" with Path.open(decrypted_file_tmp_path, "wb") as f: f.write(decoded_file) - return decrypted_file_tmp_path.name + return f"{uuid.uuid4()}{path}" def derive_key(password: str, salt: bytes) -> bytes: diff --git a/backend/app/main.py b/backend/app/main.py index 6896dbc..91577cd 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -48,6 +48,7 @@ def make_app() -> FastAPI: allow_credentials=True, allow_methods=["*"], allow_headers=["*"], + expose_headers=["*"], ) app.add_middleware(SlashNormalizerMiddleware) diff --git a/frontend/src/server/files.ts b/frontend/src/server/files.ts index 1641889..0553069 100644 --- a/frontend/src/server/files.ts +++ b/frontend/src/server/files.ts @@ -36,11 +36,13 @@ export const sendFileForSpecifiedUser = async (accessToken: string, file: File, }; export const getFileByPath = async (accessToken: string, path: string, password: string) => { - const result = await axios.get(`${BASE_URL}/files/download/${path}/`, { + const formData = new FormData(); + formData.append('file_password', password); + + const result = await axios.post(`${BASE_URL}/files/download/${path}/`, formData, { responseType: 'blob', headers: { - authorization: `Bearer ${accessToken}`, - "File-Password": password + authorization: `Bearer ${accessToken}` } }); From b6f4d07b7022ee085e1cc3cf606824303eeffae6 Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Mon, 10 Jun 2024 21:08:27 +0200 Subject: [PATCH 05/22] it worksss in a way --- backend/app/files/router.py | 16 +++++++++------- backend/app/files/service.py | 18 +++++++++++++++--- backend/app/main.py | 21 ++++++++++----------- docker-compose.yaml | 2 ++ frontend/src/server/files.ts | 17 ++++++++--------- 5 files changed, 44 insertions(+), 30 deletions(-) diff --git a/backend/app/files/router.py b/backend/app/files/router.py index 2cb425f..dd281ea 100644 --- a/backend/app/files/router.py +++ b/backend/app/files/router.py @@ -21,6 +21,7 @@ get_all_files_user, get_metadata_path, upload_file, + verify_and_decrypt_file, verify_file, verify_file_link, ) @@ -84,18 +85,19 @@ async def get_all_files( return await get_all_files_user(current_user, session) -@router.post("/download/{path}") +@router.get("/download/{path}") async def get_file( path: str, current_user: Annotated[User, Depends(get_current_user)], session: Annotated[AsyncSession, Depends(get_async_session)], - file_password: str = Form(...), + password: str = Header(None), ) -> FileResponse: - print(f"Path {path}") - filename = await verify_file(path, current_user, session) - print(f"Filename: {filename}") - full_path = decrypt_file(path, file_password, current_user) - return FileResponse(full_path, filename=filename) + # print(f"Path {path}") + # print(f"Password {password}") + # filename = await verify_file(path, current_user, session) + # print(f"Filename: {filename}") + full_path = await verify_and_decrypt_file(path, current_user, session, password) + return FileResponse(full_path, status_code=200) @router.get("/download-link/{path}") diff --git a/backend/app/files/service.py b/backend/app/files/service.py index bafa4ce..c390663 100644 --- a/backend/app/files/service.py +++ b/backend/app/files/service.py @@ -143,6 +143,17 @@ async def verify_file( return str(file.name) +async def verify_and_decrypt_file( + path: str, + current_user: Annotated[User, Depends(get_current_user)], + session: AsyncSession, + password: str | None = None, +) -> str: + _ = await verify_file(path, current_user, session) + print("verified") + return decrypt_file(path, password, current_user) + + async def verify_file_link( path: str, session: AsyncSession, current_user: User | None = None, password: str | None = None ) -> str: @@ -172,6 +183,7 @@ async def delete_file( ) -> None: async with session.begin(): await session.execute(delete(File).where(File.path == path)) + await session.commit() path_to_delete = Path(FILE_PATH) / path if path_to_delete.exists(): @@ -186,9 +198,9 @@ async def get_file_by_id(file_id: uuid.UUID, session: AsyncSession) -> File | No return file.scalar_one_or_none() -def decrypt_file(path: str, password: str | None, current_user: User) -> str: +def decrypt_file(path: str, password: str | None, current_user: User) -> Path: if password is None or password == "": - return (Path(FILE_PATH) / path).name + return Path(FILE_PATH) / path key = derive_key(password, current_user.id.bytes) fernet = Fernet(key) @@ -206,7 +218,7 @@ def decrypt_file(path: str, password: str | None, current_user: User) -> str: with Path.open(decrypted_file_tmp_path, "wb") as f: f.write(decoded_file) - return f"{uuid.uuid4()}{path}" + return decrypted_file_tmp_path def derive_key(password: str, salt: bytes) -> bytes: diff --git a/backend/app/main.py b/backend/app/main.py index 89ead7e..bc394ef 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -4,7 +4,7 @@ import uvicorn from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.cors import CORSMiddleware from .auth.router import router as auth_router from .database import run_migrations @@ -43,16 +43,6 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: def make_app() -> FastAPI: app = FastAPI(lifespan=lifespan, debug=True) - # CORS Middleware - app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - expose_headers=["*"], - ) - # URL Normalizer Middleware app.add_middleware(SlashNormalizerMiddleware) @@ -65,6 +55,15 @@ def make_app() -> FastAPI: async def root() -> str: return "Hello! The application is running." + # CORS Middleware + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + expose_headers=["*"], + ) return app diff --git a/docker-compose.yaml b/docker-compose.yaml index 834cd32..53239a7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -43,6 +43,8 @@ services: VITE_BASE_URL: ${VITE_BASE_URL} image: delemangi/synthra-frontend:latest container_name: synthra-frontend-dev + depends_on: + - backend restart: unless-stopped ports: - "3000:3000" diff --git a/frontend/src/server/files.ts b/frontend/src/server/files.ts index cd061c8..bf1aaa7 100644 --- a/frontend/src/server/files.ts +++ b/frontend/src/server/files.ts @@ -5,7 +5,7 @@ import axios from 'axios'; const BASE_URL = import.meta.env.VITE_BASE_URL; export const getFilesForSpecifiedUser = async (accessToken: string) => { - const result = await axios.get(`${BASE_URL}/files/`, { + const result = await axios.get(`${BASE_URL}/files`, { headers: { authorization: `Bearer ${accessToken}` } @@ -41,8 +41,8 @@ export const getMetadataFilePath = async (path: string) => { export const sendFileForSpecifiedUser = async ( accessToken: string, file: File, + password: string = '', isShared: boolean = false, - password: string = '' ) => { const formData = new FormData(); formData.append('file', file); @@ -52,21 +52,20 @@ export const sendFileForSpecifiedUser = async ( const result = await axios.post(`${BASE_URL}/files/`, formData, { headers: { authorization: `Bearer ${accessToken}`, - 'Content-Type': 'multipart/form-data' + 'Content-Type': 'multipart/form-data', } }); return result.data; }; -export const getFileByPath = async (accessToken: string, path: string, password: string) => { - const formData = new FormData(); - formData.append('file_password', password); - - const result = await axios.post(`${BASE_URL}/files/download/${path}/`, formData, { +export const getFileByPath = async (accessToken: string, path: string, password: string = '') => { + // const formData = new FormData(); + const result = await axios.get(`${BASE_URL}/files/download/${path}`, { responseType: 'blob', headers: { - authorization: `Bearer ${accessToken}` + authorization: `Bearer ${accessToken}`, + 'Password': password, } }); From d52d41f9a8ce3ec8324f35a1cefbd593160c477d Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Mon, 10 Jun 2024 21:17:08 +0200 Subject: [PATCH 06/22] works in a better way --- backend/app/files/router.py | 35 ++++++++++++++++++++--------------- backend/app/files/service.py | 20 +++++++------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/backend/app/files/router.py b/backend/app/files/router.py index dd281ea..747cb17 100644 --- a/backend/app/files/router.py +++ b/backend/app/files/router.py @@ -1,9 +1,10 @@ +import io from datetime import datetime, timedelta from pathlib import Path from typing import Annotated from fastapi import APIRouter, Depends, Form, Header, UploadFile -from fastapi.responses import FileResponse +from fastapi.responses import StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession from ..auth.dependencies import get_current_user @@ -16,14 +17,12 @@ from .schemas import FileUploaded, MetadataFileResponse from .service import ( create_file, - decrypt_file, delete_file, get_all_files_user, get_metadata_path, upload_file, verify_and_decrypt_file, verify_file, - verify_file_link, ) router = APIRouter(tags=["files"]) @@ -91,13 +90,15 @@ async def get_file( current_user: Annotated[User, Depends(get_current_user)], session: Annotated[AsyncSession, Depends(get_async_session)], password: str = Header(None), -) -> FileResponse: - # print(f"Path {path}") - # print(f"Password {password}") - # filename = await verify_file(path, current_user, session) - # print(f"Filename: {filename}") - full_path = await verify_and_decrypt_file(path, current_user, session, password) - return FileResponse(full_path, status_code=200) +) -> StreamingResponse: + print(path) + file = await verify_and_decrypt_file(path, current_user, session, password) + file_object = io.BytesIO(file) + return StreamingResponse( + file_object, + media_type="application/octet-stream", + headers={"Content-Disposition": f"attachment; filename={path}"}, + ) @router.get("/download-link/{path}") @@ -105,11 +106,15 @@ async def get_file_link( path: str, session: Annotated[AsyncSession, Depends(get_async_session)], current_user: Annotated[User, Depends(get_current_user)], - file_password: Annotated[str | None, Header()], -) -> FileResponse: - filename = await verify_file_link(path, session, current_user, file_password) - full_path = decrypt_file(path, file_password, current_user) - return FileResponse(Path(FILE_PATH) / full_path, filename=filename) + password: Annotated[str | None, Header()], +) -> StreamingResponse: + file = await verify_and_decrypt_file(path, current_user, session, password) + file_object = io.BytesIO(file) + return StreamingResponse( + file_object, + media_type="application/octet-stream", + headers={"Content-Disposition": f"attachment; filename={path}"}, + ) @router.get("/metadata/{path}", response_model=MetadataFileResponse) diff --git a/backend/app/files/service.py b/backend/app/files/service.py index c390663..5c6a765 100644 --- a/backend/app/files/service.py +++ b/backend/app/files/service.py @@ -148,9 +148,8 @@ async def verify_and_decrypt_file( current_user: Annotated[User, Depends(get_current_user)], session: AsyncSession, password: str | None = None, -) -> str: +) -> bytes: _ = await verify_file(path, current_user, session) - print("verified") return decrypt_file(path, password, current_user) @@ -198,27 +197,22 @@ async def get_file_by_id(file_id: uuid.UUID, session: AsyncSession) -> File | No return file.scalar_one_or_none() -def decrypt_file(path: str, password: str | None, current_user: User) -> Path: +def decrypt_file(path: str, password: str | None, current_user: User) -> bytes: + with Path.open(Path(FILE_PATH) / path, "rb") as f: + contents = f.read() + if password is None or password == "": - return Path(FILE_PATH) / path + return contents key = derive_key(password, current_user.id.bytes) fernet = Fernet(key) - with Path.open(Path(FILE_PATH) / path, "rb") as f: - contents = f.read() - try: decoded_file = fernet.decrypt(contents) except InvalidToken as err: raise NO_ACCESS_EXCEPTION from err - decrypted_file_tmp_path = Path(FILE_PATH) / f"{uuid.uuid4()}{path}" - - with Path.open(decrypted_file_tmp_path, "wb") as f: - f.write(decoded_file) - - return decrypted_file_tmp_path + return decoded_file def derive_key(password: str, salt: bytes) -> bytes: From 6f848dd5ae7ff218f97a2d768e5dbec896b2ce47 Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Mon, 10 Jun 2024 22:01:15 +0200 Subject: [PATCH 07/22] frontend pt 1 --- backend/app/files/service.py | 7 +++++-- .../src/lib/components/user/FileRow.svelte | 1 - frontend/src/routes/user/home/+page.svelte | 18 ++++++++++++------ frontend/src/server/files.ts | 3 +-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/backend/app/files/service.py b/backend/app/files/service.py index 5c6a765..3aa1482 100644 --- a/backend/app/files/service.py +++ b/backend/app/files/service.py @@ -36,7 +36,7 @@ async def upload_file( async with session.begin(): contents = await file.read() - if password is not None: + if password is not None and password != "": key = derive_key(password, current_user.id.bytes) fernet = Fernet(key) contents = fernet.encrypt(contents) @@ -47,7 +47,7 @@ async def upload_file( file_db = File( name=file.filename, path=file_path, - encrypted=password is not None, + encrypted=password is not None and password != "", size=file.size, timestamp=datetime.now(), expiration=datetime.now() + timedelta(days=14), @@ -173,6 +173,9 @@ async def verify_file_link( ): raise NO_ACCESS_EXCEPTION + if bool(file.encrypted) and (password is None or password == ""): + raise NO_ACCESS_EXCEPTION + return str(file.name) diff --git a/frontend/src/lib/components/user/FileRow.svelte b/frontend/src/lib/components/user/FileRow.svelte index 35c00d9..0b18129 100644 --- a/frontend/src/lib/components/user/FileRow.svelte +++ b/frontend/src/lib/components/user/FileRow.svelte @@ -305,7 +305,6 @@ Download File - diff --git a/frontend/src/routes/user/home/+page.svelte b/frontend/src/routes/user/home/+page.svelte index 5d24f16..a174338 100644 --- a/frontend/src/routes/user/home/+page.svelte +++ b/frontend/src/routes/user/home/+page.svelte @@ -20,6 +20,8 @@ let filesToUpload: FileList | null = null; let privateFile = true; + let passwordLock = false; + let filePassword = ''; const sendData = async () => { const accessToken = localStorage.getItem('accessToken'); @@ -34,8 +36,8 @@ } try { - await sendFileForSpecifiedUser(accessToken, filesToUpload[0], uploadFilePassword, privateFile); - uploadFilePassword = '' + await sendFileForSpecifiedUser(accessToken, filesToUpload[0], filePassword, privateFile); + filePassword = '' window.location.reload(); } catch (error) { if (!isAxiosError(error)) { @@ -74,7 +76,6 @@ $: ({ classes, getStyles } = useStyles()); let userFiles: FileMetadata[] = []; - let uploadFilePassword = ''; let visible = false; @@ -135,10 +136,15 @@ Private? - - + + filePassword = ''}> + Lock with password? + + {#if passwordLock} + + {/if} - diff --git a/frontend/src/server/files.ts b/frontend/src/server/files.ts index bf1aaa7..a78dd9e 100644 --- a/frontend/src/server/files.ts +++ b/frontend/src/server/files.ts @@ -59,8 +59,7 @@ export const sendFileForSpecifiedUser = async ( return result.data; }; -export const getFileByPath = async (accessToken: string, path: string, password: string = '') => { - // const formData = new FormData(); +export const getFileByPath = async (accessToken: string, path: string, password: string | null = null) => { const result = await axios.get(`${BASE_URL}/files/download/${path}`, { responseType: 'blob', headers: { From 4095ff3e748063de33ccb4bc3dddca763bd8ee50 Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Mon, 10 Jun 2024 22:29:48 +0200 Subject: [PATCH 08/22] (mostly) frontend pt 2 --- backend/app/files/router.py | 16 ----------- .../src/lib/components/user/FileRow.svelte | 7 ++--- frontend/src/routes/download/+page.svelte | 27 ++++++++++++++----- frontend/src/server/files.ts | 2 +- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/backend/app/files/router.py b/backend/app/files/router.py index 747cb17..79fb3e2 100644 --- a/backend/app/files/router.py +++ b/backend/app/files/router.py @@ -101,22 +101,6 @@ async def get_file( ) -@router.get("/download-link/{path}") -async def get_file_link( - path: str, - session: Annotated[AsyncSession, Depends(get_async_session)], - current_user: Annotated[User, Depends(get_current_user)], - password: Annotated[str | None, Header()], -) -> StreamingResponse: - file = await verify_and_decrypt_file(path, current_user, session, password) - file_object = io.BytesIO(file) - return StreamingResponse( - file_object, - media_type="application/octet-stream", - headers={"Content-Disposition": f"attachment; filename={path}"}, - ) - - @router.get("/metadata/{path}", response_model=MetadataFileResponse) async def get_file_metadata( path: str, diff --git a/frontend/src/lib/components/user/FileRow.svelte b/frontend/src/lib/components/user/FileRow.svelte index 0b18129..1bb52e4 100644 --- a/frontend/src/lib/components/user/FileRow.svelte +++ b/frontend/src/lib/components/user/FileRow.svelte @@ -303,18 +303,15 @@ {#if isDownloadWindowVisible} - + Download File - - - + - diff --git a/frontend/src/routes/download/+page.svelte b/frontend/src/routes/download/+page.svelte index bc0aac1..f0f972a 100644 --- a/frontend/src/routes/download/+page.svelte +++ b/frontend/src/routes/download/+page.svelte @@ -5,6 +5,7 @@ Button, Flex, Text, + TextInput, Title, createStyles, type DefaultTheme @@ -42,6 +43,7 @@ let filePath: string | null = null; let fileMetadata: FileMetadata | null = null; let fileUrl: string | null = null; + let downloadFilePassword: string = ''; onMount(async () => { const urlParams = new URLSearchParams(window.location.search); @@ -60,11 +62,7 @@ return; } - let retrievedFile = await getFileByPath(accessToken, filePath); - if (retrievedFile) { - fileUrl = URL.createObjectURL(retrievedFile); - } } catch (error) { alert('The file does not exist, or has expired.'); window.location.href = '/'; @@ -72,6 +70,17 @@ }); const downloadFile = async () => { + + if(filePath == null) { + alert('An error occurred while downloading the file.'); + return; + } + + let retrievedFile = await getFileByPath(localStorage.getItem('accessToken'), filePath, downloadFilePassword); + + if (retrievedFile) { + fileUrl = URL.createObjectURL(retrievedFile); + } if (!filePath) { alert('An error occurred while downloading the file.'); return; @@ -109,7 +118,7 @@
- + Download File {#if fileMetadata} @@ -121,7 +130,13 @@ Loading... {/if} - + {#if fileMetadata?.encrypted} + + {/if} +
{#if fileUrl && isFileTypeSupported(filePath)}
{ +export const getFileByPath = async (accessToken: string | null, path: string, password: string | null = null) => { const result = await axios.get(`${BASE_URL}/files/download/${path}`, { responseType: 'blob', headers: { From 5b7fa0de814709a69d65f15def808d87738e9887 Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Mon, 10 Jun 2024 22:37:57 +0200 Subject: [PATCH 09/22] fix problems --- backend/poetry.lock | 107 +++++++++--------- .../src/lib/components/user/FileRow.svelte | 19 ++-- 2 files changed, 60 insertions(+), 66 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index 42209b6..35488b5 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiosqlite" @@ -1134,57 +1134,57 @@ files = [ [[package]] name = "orjson" -version = "3.10.3" +version = "3.10.4" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3"}, - {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7"}, - {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d"}, - {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7"}, - {file = "orjson-3.10.3-cp310-none-win32.whl", hash = "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109"}, - {file = "orjson-3.10.3-cp310-none-win_amd64.whl", hash = "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b"}, - {file = "orjson-3.10.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf"}, - {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16"}, - {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08"}, - {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5"}, - {file = "orjson-3.10.3-cp311-none-win32.whl", hash = "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b"}, - {file = "orjson-3.10.3-cp311-none-win_amd64.whl", hash = "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5"}, - {file = "orjson-3.10.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0"}, - {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42"}, - {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069"}, - {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534"}, - {file = "orjson-3.10.3-cp312-none-win32.whl", hash = "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0"}, - {file = "orjson-3.10.3-cp312-none-win_amd64.whl", hash = "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0"}, - {file = "orjson-3.10.3-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5"}, - {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754"}, - {file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195"}, - {file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b"}, - {file = "orjson-3.10.3-cp38-none-win32.whl", hash = "sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134"}, - {file = "orjson-3.10.3-cp38-none-win_amd64.whl", hash = "sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290"}, - {file = "orjson-3.10.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78"}, - {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d"}, - {file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25"}, - {file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8"}, - {file = "orjson-3.10.3-cp39-none-win32.whl", hash = "sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063"}, - {file = "orjson-3.10.3-cp39-none-win_amd64.whl", hash = "sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912"}, - {file = "orjson-3.10.3.tar.gz", hash = "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818"}, + {file = "orjson-3.10.4-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:afca963f19ca60c7aedadea9979f769139127288dd58ccf3f7c5e8e6dc62cabf"}, + {file = "orjson-3.10.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b112eff36ba7ccc7a9d6b87e17b9d6bde4312d05e3ddf66bf5662481dee846"}, + {file = "orjson-3.10.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02b192eaba048b1039eca9a0cef67863bd5623042f5c441889a9957121d97e14"}, + {file = "orjson-3.10.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:827c3d0e4fc44242c82bfdb1a773235b8c0575afee99a9fa9a8ce920c14e440f"}, + {file = "orjson-3.10.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca8ec09724f10ec209244caeb1f9f428b6bb03f2eda9ed5e2c4dd7f2b7fabd44"}, + {file = "orjson-3.10.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8eaa5d531a8fde11993cbcb27e9acf7d9c457ba301adccb7fa3a021bfecab46c"}, + {file = "orjson-3.10.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e112aa7fc4ea67367ec5e86c39a6bb6c5719eddc8f999087b1759e765ddaf2d4"}, + {file = "orjson-3.10.4-cp310-none-win32.whl", hash = "sha256:1538844fb88446c42da3889f8c4ecce95a630b5a5ba18ecdfe5aea596f4dff21"}, + {file = "orjson-3.10.4-cp310-none-win_amd64.whl", hash = "sha256:de02811903a2e434127fba5389c3cc90f689542339a6e52e691ab7f693407b5a"}, + {file = "orjson-3.10.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:358afaec75de7237dfea08e6b1b25d226e33a1e3b6dc154fc99eb697f24a1ffa"}, + {file = "orjson-3.10.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb4e292c3198ab3d93e5f877301d2746be4ca0ba2d9c513da5e10eb90e19ff52"}, + {file = "orjson-3.10.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c39e57cf6323a39238490092985d5d198a7da4a3be013cc891a33fef13a536e"}, + {file = "orjson-3.10.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f86df433fc01361ff9270ad27455ce1ad43cd05e46de7152ca6adb405a16b2f6"}, + {file = "orjson-3.10.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c9966276a2c97e93e6cbe8286537f88b2a071827514f0d9d47a0aefa77db458"}, + {file = "orjson-3.10.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c499a14155a1f5a1e16e0cd31f6cf6f93965ac60a0822bc8340e7e2d3dac1108"}, + {file = "orjson-3.10.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3087023ce904a327c29487eb7e1f2c060070e8dbb9a3991b8e7952a9c6e62f38"}, + {file = "orjson-3.10.4-cp311-none-win32.whl", hash = "sha256:f965893244fe348b59e5ce560693e6dd03368d577ce26849b5d261ce31c70101"}, + {file = "orjson-3.10.4-cp311-none-win_amd64.whl", hash = "sha256:c212f06fad6aa6ce85d5665e91a83b866579f29441a47d3865c57329c0857357"}, + {file = "orjson-3.10.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d0965a8b0131959833ca8a65af60285995d57ced0de2fd8f16fc03235975d238"}, + {file = "orjson-3.10.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27b64695d9f2aef3ae15a0522e370ec95c946aaea7f2c97a1582a62b3bdd9169"}, + {file = "orjson-3.10.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:867d882ddee6a20be4c8b03ae3d2b0333894d53ad632d32bd9b8123649577171"}, + {file = "orjson-3.10.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0667458f8a8ceb6dee5c08fec0b46195f92c474cbbec71dca2a6b7fd5b67b8d"}, + {file = "orjson-3.10.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3eac9befc4eaec1d1ff3bba6210576be4945332dde194525601c5ddb5c060d3"}, + {file = "orjson-3.10.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4343245443552eae240a33047a6d1bcac7a754ad4b1c57318173c54d7efb9aea"}, + {file = "orjson-3.10.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30153e269eea43e98918d4d462a36a7065031d9246407dfff2579a4e457515c1"}, + {file = "orjson-3.10.4-cp312-none-win32.whl", hash = "sha256:1a7d092ee043abf3db19c2183115e80676495c9911843fdb3ebd48ca7b73079e"}, + {file = "orjson-3.10.4-cp312-none-win_amd64.whl", hash = "sha256:07a2adbeb8b9efe6d68fc557685954a1f19d9e33f5cc018ae1a89e96647c1b65"}, + {file = "orjson-3.10.4-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f5a746f3d908bce1a1e347b9ca89864047533bdfab5a450066a0315f6566527b"}, + {file = "orjson-3.10.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:465b4a8a3e459f8d304c19071b4badaa9b267c59207a005a7dd9dfe13d3a423f"}, + {file = "orjson-3.10.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35858d260728c434a3d91b60685ab32418318567e8902039837e1c2af2719e0b"}, + {file = "orjson-3.10.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a5ba090d40c4460312dd69c232b38c2ff67a823185cfe667e841c9dd5c06841"}, + {file = "orjson-3.10.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dde86755d064664e62e3612a166c28298aa8dfd35a991553faa58855ae739cc"}, + {file = "orjson-3.10.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:020a9e9001cfec85c156ef3b185ff758b62ef986cefdb8384c4579facd5ce126"}, + {file = "orjson-3.10.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3bf8e6e3388a2e83a86466c912387e0f0a765494c65caa7e865f99969b76ba0d"}, + {file = "orjson-3.10.4-cp38-none-win32.whl", hash = "sha256:c5a1cca6a4a3129db3da68a25dc0a459a62ae58e284e363b35ab304202d9ba9e"}, + {file = "orjson-3.10.4-cp38-none-win_amd64.whl", hash = "sha256:ecd97d98d7bee3e3d51d0b51c92c457f05db4993329eea7c69764f9820e27eb3"}, + {file = "orjson-3.10.4-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:71362daa330a2fc85553a1469185ac448547392a8f83d34e67779f8df3a52743"}, + {file = "orjson-3.10.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d24b59d1fecb0fd080c177306118a143f7322335309640c55ed9580d2044e363"}, + {file = "orjson-3.10.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e906670aea5a605b083ebb58d575c35e88cf880fa372f7cedaac3d51e98ff164"}, + {file = "orjson-3.10.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ce32ed4bc4d632268e4978e595fe5ea07e026b751482b4a0feec48f66a90abc"}, + {file = "orjson-3.10.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dcd34286246e0c5edd0e230d1da2daab2c1b465fcb6bac85b8d44057229d40a"}, + {file = "orjson-3.10.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c45d4b8c403e50beedb1d006a8916d9910ed56bceaf2035dc253618b44d0a161"}, + {file = "orjson-3.10.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:aaed3253041b5002a4f5bfdf6f7b5cce657d974472b0699a469d439beba40381"}, + {file = "orjson-3.10.4-cp39-none-win32.whl", hash = "sha256:9a4f41b7dbf7896f8dbf559b9b43dcd99e31e0d49ac1b59d74f52ce51ab10eb9"}, + {file = "orjson-3.10.4-cp39-none-win_amd64.whl", hash = "sha256:6c4eb7d867ed91cb61e6514cb4f457aa01d7b0fd663089df60a69f3d38b69d4c"}, + {file = "orjson-3.10.4.tar.gz", hash = "sha256:c912ed25b787c73fe994a5decd81c3f3b256599b8a87d410d799d5d52013af2a"}, ] [[package]] @@ -1203,13 +1203,13 @@ attrs = ">=19.2.0" [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -1698,7 +1698,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -2436,4 +2435,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11.0" -content-hash = "2783c05c1cf1b9027a2b395ae861733d7a46f63f0d75283c1d1b7a420c3a4987" +content-hash = "6168819ed66992a324208fc944179e351ab90232c27b30135703694b01fb7264" diff --git a/frontend/src/lib/components/user/FileRow.svelte b/frontend/src/lib/components/user/FileRow.svelte index 1bb52e4..2754f25 100644 --- a/frontend/src/lib/components/user/FileRow.svelte +++ b/frontend/src/lib/components/user/FileRow.svelte @@ -59,10 +59,8 @@ }); const downloadFile = () => { - if(file.encrypted) - isDownloadWindowVisible = true; - else - getFile(); + if (file.encrypted) isDownloadWindowVisible = true; + else getFile(); }; const getFile = async () => { const accessToken = localStorage.getItem('accessToken'); @@ -195,7 +193,6 @@ }); $: ({ classes, getStyles } = useStyles()); - $: ({ classes, getStyles } = useStyles()); @@ -305,13 +302,11 @@ Download File - - - + + + From 7e2a8d23e8b4b095c13e04e3c004ec63576067d0 Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Mon, 10 Jun 2024 22:43:42 +0200 Subject: [PATCH 10/22] fix problems --- backend/app/files/service.py | 2 +- frontend/src/routes/download/+page.svelte | 16 ++++++++++------ frontend/src/routes/user/home/+page.svelte | 13 +++++++++---- frontend/src/server/files.ts | 12 ++++++++---- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/backend/app/files/service.py b/backend/app/files/service.py index 3aa1482..a997d92 100644 --- a/backend/app/files/service.py +++ b/backend/app/files/service.py @@ -163,7 +163,7 @@ async def verify_file_link( if file is None: raise NOT_FOUND_EXCEPTION - if file.encrypted and password is None: # type: ignore + if bool(file.encrypted) and password is None: raise NOT_FOUND_EXCEPTION if ( diff --git a/frontend/src/routes/download/+page.svelte b/frontend/src/routes/download/+page.svelte index f0f972a..7065e53 100644 --- a/frontend/src/routes/download/+page.svelte +++ b/frontend/src/routes/download/+page.svelte @@ -61,8 +61,6 @@ alert('You need to be logged in.'); return; } - - } catch (error) { alert('The file does not exist, or has expired.'); window.location.href = '/'; @@ -70,13 +68,16 @@ }); const downloadFile = async () => { - - if(filePath == null) { + if (filePath == null) { alert('An error occurred while downloading the file.'); return; } - let retrievedFile = await getFileByPath(localStorage.getItem('accessToken'), filePath, downloadFilePassword); + let retrievedFile = await getFileByPath( + localStorage.getItem('accessToken'), + filePath, + downloadFilePassword + ); if (retrievedFile) { fileUrl = URL.createObjectURL(retrievedFile); @@ -136,7 +137,10 @@ bind:value={downloadFilePassword} /> {/if} - +
{#if fileUrl && isFileTypeSupported(filePath)}
Private? - filePassword = ''}> + (filePassword = '')}> Lock with password? {#if passwordLock} - + {/if} - diff --git a/frontend/src/server/files.ts b/frontend/src/server/files.ts index eb48dfa..5f3c8ee 100644 --- a/frontend/src/server/files.ts +++ b/frontend/src/server/files.ts @@ -42,7 +42,7 @@ export const sendFileForSpecifiedUser = async ( accessToken: string, file: File, password: string = '', - isShared: boolean = false, + isShared: boolean = false ) => { const formData = new FormData(); formData.append('file', file); @@ -52,19 +52,23 @@ export const sendFileForSpecifiedUser = async ( const result = await axios.post(`${BASE_URL}/files/`, formData, { headers: { authorization: `Bearer ${accessToken}`, - 'Content-Type': 'multipart/form-data', + 'Content-Type': 'multipart/form-data' } }); return result.data; }; -export const getFileByPath = async (accessToken: string | null, path: string, password: string | null = null) => { +export const getFileByPath = async ( + accessToken: string | null, + path: string, + password: string | null = null +) => { const result = await axios.get(`${BASE_URL}/files/download/${path}`, { responseType: 'blob', headers: { authorization: `Bearer ${accessToken}`, - 'Password': password, + Password: password } }); From d62918561f3e5fdfec1eaf8f0ab8869b0f228d3f Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska <86167204+amatanasovska@users.noreply.github.com> Date: Mon, 10 Jun 2024 22:47:26 +0200 Subject: [PATCH 11/22] Update sonar-project.properties --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 9807398..af6c82d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,5 +1,5 @@ -sonar.projectKey=synthra sonar.organization=delemangi sonar.sources=. +sonar.exclusions=**/versions/** sonar.python.version=3.11 From 34768b4dd93d9d2067bf7c072d646fcf2ff7b7b0 Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska <86167204+amatanasovska@users.noreply.github.com> Date: Mon, 10 Jun 2024 22:47:52 +0200 Subject: [PATCH 12/22] Update sonar-project.properties --- sonar-project.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/sonar-project.properties b/sonar-project.properties index af6c82d..c3fd832 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,3 +1,4 @@ +sonar.projectKey=synthra sonar.organization=delemangi sonar.sources=. From 82e7814c642346db02b67c71811d6dc79fd890f0 Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Tue, 11 Jun 2024 00:51:22 +0200 Subject: [PATCH 13/22] initial 2fa impl --- .../versions/0f18e18f9ae9_add_2fa_auth.py | 30 ++++++++++ backend/app/auth/models.py | 1 + backend/app/auth/router.py | 18 +++++- backend/app/auth/schemas.py | 4 ++ backend/app/auth/service.py | 11 +++- backend/poetry.lock | 16 +++++- backend/pyproject.toml | 1 + frontend/package-lock.json | 17 ++++++ frontend/package.json | 1 + frontend/src/lib/types/Code2FA.ts | 3 + frontend/src/routes/user/2fa/+page.svelte | 55 +++++++++++++++++++ frontend/src/server/auth.ts | 12 +++- 12 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 backend/alembic/versions/0f18e18f9ae9_add_2fa_auth.py create mode 100644 frontend/src/lib/types/Code2FA.ts create mode 100644 frontend/src/routes/user/2fa/+page.svelte diff --git a/backend/alembic/versions/0f18e18f9ae9_add_2fa_auth.py b/backend/alembic/versions/0f18e18f9ae9_add_2fa_auth.py new file mode 100644 index 0000000..23c60cb --- /dev/null +++ b/backend/alembic/versions/0f18e18f9ae9_add_2fa_auth.py @@ -0,0 +1,30 @@ +"""Add 2FA auth + +Revision ID: 0f18e18f9ae9 +Revises: adbfe149f408 +Create Date: 2024-06-11 00:00:10.056063 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '0f18e18f9ae9' +down_revision: Union[str, None] = 'adbfe149f408' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('code_2fa', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'code_2fa') + # ### end Alembic commands ### diff --git a/backend/app/auth/models.py b/backend/app/auth/models.py index f209914..14229a4 100644 --- a/backend/app/auth/models.py +++ b/backend/app/auth/models.py @@ -27,6 +27,7 @@ class User(Base): "Webhook", back_populates="user", cascade="all, delete-orphan", lazy="selectin" ) shared_files = relationship("Share", back_populates="user", lazy="selectin") + code_2fa = Column(String, nullable=True) def has_remaining_quota(self: Self) -> bool: return bool(self.quota != 0) diff --git a/backend/app/auth/router.py b/backend/app/auth/router.py index 4643e76..717c807 100644 --- a/backend/app/auth/router.py +++ b/backend/app/auth/router.py @@ -7,13 +7,14 @@ from ..database import get_async_session from ..schemas import RequestStatus from .exceptions import CREDENTIALS_EXCEPTION -from .schemas import Token, User +from .schemas import Code2FA, Token, User from .service import ( authenticate_user, create_access_token, create_user, oauth2_scheme, remove_token, + update_2fa_code, ) router = APIRouter(tags=["auth"]) @@ -60,3 +61,18 @@ async def register( ) -> RequestStatus: user = await create_user(user_schema.username, user_schema.password, 30, session) return RequestStatus(message=f"User {user.username} registered successfully") + + +@router.post("/2fa") +async def get_2fa_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()], + session: Annotated[AsyncSession, Depends(get_async_session)], +) -> Code2FA: + user = await authenticate_user(form_data.username, form_data.password, session) + + if not user: + raise CREDENTIALS_EXCEPTION + + code_2fa = await update_2fa_code(user, session) + + return Code2FA(code=code_2fa) diff --git a/backend/app/auth/schemas.py b/backend/app/auth/schemas.py index 9f109aa..d2943ef 100644 --- a/backend/app/auth/schemas.py +++ b/backend/app/auth/schemas.py @@ -17,3 +17,7 @@ class Token(BaseModel): class TokenData(BaseModel): username: str | None = None + + +class Code2FA(BaseModel): + code: str diff --git a/backend/app/auth/service.py b/backend/app/auth/service.py index d841fa3..6e39c45 100644 --- a/backend/app/auth/service.py +++ b/backend/app/auth/service.py @@ -4,10 +4,11 @@ from datetime import UTC, datetime, timedelta from uuid import UUID +import pyotp from fastapi.security import OAuth2PasswordBearer from jose import jwt from passlib.context import CryptContext -from sqlalchemy import ColumnElement, and_, delete, select +from sqlalchemy import ColumnElement, and_, delete, select, update from sqlalchemy.ext.asyncio import AsyncSession from ..database import get_async_session @@ -58,6 +59,14 @@ async def authenticate_user(username: str, password: str, session: AsyncSession) ) +async def update_2fa_code(user: User, session: AsyncSession) -> str: + code = pyotp.random_base32() + await session.execute(update(User).where(User.id == user.id).values(code_2fa=code)) + await session.commit() + + return code + + async def create_access_token( session: AsyncSession, data: dict[str, str | int | datetime], diff --git a/backend/poetry.lock b/backend/poetry.lock index 35488b5..a88b074 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1557,6 +1557,20 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyotp" +version = "2.9.0" +description = "Python One Time Password Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyotp-2.9.0-py3-none-any.whl", hash = "sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612"}, + {file = "pyotp-2.9.0.tar.gz", hash = "sha256:346b6642e0dbdde3b4ff5a930b664ca82abfa116356ed48cc42c7d6590d36f63"}, +] + +[package.extras] +test = ["coverage", "mypy", "ruff", "wheel"] + [[package]] name = "pytest" version = "8.2.2" @@ -2435,4 +2449,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11.0" -content-hash = "6168819ed66992a324208fc944179e351ab90232c27b30135703694b01fb7264" +content-hash = "21a46c6712879fbbdd8c90a823c70e1a52372618c9a0560f9011ad9449b380a3" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 60a8ac4..3692c20 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -28,6 +28,7 @@ bcrypt = "^4.1.3" aiosqlite = "^0.20.0" pytest-mock = "^3.14.0" cryptography = "^42.0.8" +pyotp = "^2.9.0" [tool.poetry.group.dev.dependencies] mypy = "^1.10.0" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b9ab208..76524c9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.0.1", "dependencies": { + "@svelte-put/qr": "^1.2.1", "@svelteuidev/composables": "^0.15.6", "@svelteuidev/core": "^0.15.6", "axios": "^1.7.2", @@ -1102,6 +1103,17 @@ "integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==", "license": "MIT" }, + "node_modules/@svelte-put/qr": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@svelte-put/qr/-/qr-1.2.1.tgz", + "integrity": "sha512-VIAze6mCVWdiyq6xnMFhbD3dbTvz9CDFVN3lVkDvx6SYFcstgHuIyb7Pi3aJIr2ClDc4U+W2U+SGhFuQBskvBg==", + "dependencies": { + "qrcode-generator": "^1.4.4" + }, + "peerDependencies": { + "svelte": "^3.55.0 || ^4.0.0 || ^5.0.0" + } + }, "node_modules/@sveltejs/adapter-node": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.0.1.tgz", @@ -3960,6 +3972,11 @@ "node": ">=6" } }, + "node_modules/qrcode-generator": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz", + "integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index dcf0d32..a08de4d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ }, "type": "module", "dependencies": { + "@svelte-put/qr": "^1.2.1", "@svelteuidev/composables": "^0.15.6", "@svelteuidev/core": "^0.15.6", "axios": "^1.7.2", diff --git a/frontend/src/lib/types/Code2FA.ts b/frontend/src/lib/types/Code2FA.ts new file mode 100644 index 0000000..0d6d1bf --- /dev/null +++ b/frontend/src/lib/types/Code2FA.ts @@ -0,0 +1,3 @@ +export type Code2FA = { + code: string; +}; diff --git a/frontend/src/routes/user/2fa/+page.svelte b/frontend/src/routes/user/2fa/+page.svelte new file mode 100644 index 0000000..35f8aff --- /dev/null +++ b/frontend/src/routes/user/2fa/+page.svelte @@ -0,0 +1,55 @@ + + +{#if code == null} +
+

Please log in again to get a new 2FA token

+ + +
+
+ +
+
+
+{/if} + +{#if code != null} +

2FA token {code}

+ + +{/if} diff --git a/frontend/src/server/auth.ts b/frontend/src/server/auth.ts index e7cfd6e..2a017cd 100644 --- a/frontend/src/server/auth.ts +++ b/frontend/src/server/auth.ts @@ -1,4 +1,5 @@ import type { AccessToken } from '$lib/types/AccessToken'; +import type { Code2FA } from '$lib/types/Code2FA'; import axios from 'axios'; const BASE_URL = import.meta.env.VITE_BASE_URL; @@ -23,7 +24,7 @@ export const register = async (username: string, password: string) => { }; export const logout = async (token: string) => { - const result = await axios.post(`${BASE_URL}/auth/logout`, { + const result = await axios.post(`${BASE_URL}/auth/logout`, new FormData(), { headers: { authorization: `Bearer ${token}` } @@ -31,3 +32,12 @@ export const logout = async (token: string) => { return result; }; + +export const get2faToken = async (username: string, password: string) => { + const formData = new FormData(); + formData.append('username', username); + formData.append('password', password); + const result = await axios.post(`${BASE_URL}/auth/2fa`, formData); + + return result; +}; From f67e2f4710a943da87b0f7a69e12c6d6aa8b5bc1 Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Tue, 11 Jun 2024 01:19:09 +0200 Subject: [PATCH 14/22] 2fa full impl --- backend/app/auth/exceptions.py | 4 ++ backend/app/auth/router.py | 9 +++- backend/app/auth/service.py | 10 ++++ frontend/src/routes/auth/login/+page.svelte | 60 +++++++++++++++------ frontend/src/server/auth.ts | 3 +- 5 files changed, 67 insertions(+), 19 deletions(-) diff --git a/backend/app/auth/exceptions.py b/backend/app/auth/exceptions.py index 4e93782..5221102 100644 --- a/backend/app/auth/exceptions.py +++ b/backend/app/auth/exceptions.py @@ -15,3 +15,7 @@ USERNAME_TAKEN_EXCEPTION = HTTPException( status_code=status.HTTP_406_NOT_ACCEPTABLE, detail="Username already taken." ) + +AUTHENTICATION_2FA_EXCEPTION = HTTPException( + status_code=status.HTTP_302_FOUND, detail="User found. 2FA authentication required." +) diff --git a/backend/app/auth/router.py b/backend/app/auth/router.py index 717c807..8a43db1 100644 --- a/backend/app/auth/router.py +++ b/backend/app/auth/router.py @@ -1,12 +1,12 @@ from typing import Annotated -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Form from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.ext.asyncio import AsyncSession from ..database import get_async_session from ..schemas import RequestStatus -from .exceptions import CREDENTIALS_EXCEPTION +from .exceptions import AUTHENTICATION_2FA_EXCEPTION, CREDENTIALS_EXCEPTION from .schemas import Code2FA, Token, User from .service import ( authenticate_user, @@ -15,6 +15,7 @@ oauth2_scheme, remove_token, update_2fa_code, + verify_2fa_code, ) router = APIRouter(tags=["auth"]) @@ -36,12 +37,16 @@ async def test(session: Annotated[AsyncSession, Depends(get_async_session)]) -> async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()], session: Annotated[AsyncSession, Depends(get_async_session)], + code_2fa: str = Form(None), ) -> dict[str, str]: user = await authenticate_user(form_data.username, form_data.password, session) if not user: raise CREDENTIALS_EXCEPTION + if not verify_2fa_code(user, code_2fa): + raise AUTHENTICATION_2FA_EXCEPTION + access_token = await create_access_token(session, data={"sub": str(user.username)}) return {"access_token": access_token, "token_type": "bearer"} diff --git a/backend/app/auth/service.py b/backend/app/auth/service.py index 6e39c45..f800912 100644 --- a/backend/app/auth/service.py +++ b/backend/app/auth/service.py @@ -59,6 +59,16 @@ async def authenticate_user(username: str, password: str, session: AsyncSession) ) +def verify_2fa_code(user: User, code: str | None) -> bool: + if user.code_2fa is None and code is None: + return True + if user.code_2fa is None or code is None: + return False + + totp = pyotp.TOTP(str(user.code_2fa)) + return totp.verify(code) + + async def update_2fa_code(user: User, session: AsyncSession) -> str: code = pyotp.random_base32() await session.execute(update(User).where(User.id == user.id).values(code_2fa=code)) diff --git a/frontend/src/routes/auth/login/+page.svelte b/frontend/src/routes/auth/login/+page.svelte index 04191a0..d02c928 100644 --- a/frontend/src/routes/auth/login/+page.svelte +++ b/frontend/src/routes/auth/login/+page.svelte @@ -4,14 +4,16 @@ import { login } from '../../../server/auth'; import { onMount } from 'svelte'; - let username = ''; - let password = ''; + let username: string = ''; + let password: string = ''; + let code2FA: string | null = null; + let code2FAInput: boolean = false; const handleSubmit = async () => { let response; try { - response = await login(username, password); + response = await login(username, password, code2FA); localStorage.setItem('accessToken', response.data.access_token); localStorage.setItem('username', username); @@ -22,6 +24,15 @@ return; } + if (error.response?.status === 302) { + if (code2FAInput) { + alert('Invalid 2FA code. Try again.'); + return; + } + code2FAInput = true; + return; + } + if (error.response?.status === 401) { alert('Invalid username or password.'); return; @@ -40,17 +51,34 @@ }); -
- - -
-
- +{#if !code2FAInput} +
+ + +
+
+ +
+
+ + No account? Register! + +
+{/if} + +{#if code2FAInput} +

2FA authentication enabled

+

Please open your authentication app and enter the code.

+
+ +
+
+ +
+
-
- - No account? Register! - -
+{/if} diff --git a/frontend/src/server/auth.ts b/frontend/src/server/auth.ts index 2a017cd..fdaf3de 100644 --- a/frontend/src/server/auth.ts +++ b/frontend/src/server/auth.ts @@ -4,10 +4,11 @@ import axios from 'axios'; const BASE_URL = import.meta.env.VITE_BASE_URL; -export const login = async (username: string, password: string) => { +export const login = async (username: string, password: string, code2FA: string | null) => { const formData = new FormData(); formData.append('username', username); formData.append('password', password); + formData.append('code_2fa', code2FA == null ? '' : code2FA); const result = await axios.post(`${BASE_URL}/auth/login`, formData); From bc3f725c34c131ccb1256cfa9c34198fef02bcc6 Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Tue, 11 Jun 2024 02:21:48 +0200 Subject: [PATCH 15/22] some improvements --- backend/app/auth/router.py | 31 ++++++++++++- backend/app/auth/schemas.py | 6 +++ backend/app/auth/service.py | 5 ++ frontend/src/lib/components/Header.svelte | 32 ++++++++++--- frontend/src/lib/types/UserMetadata.ts | 5 ++ frontend/src/routes/user/2fa/+page.svelte | 43 +++++++++++++---- frontend/src/routes/user/account/+page.svelte | 46 +++++++++++++++++++ frontend/src/server/auth.ts | 19 ++++++++ 8 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 frontend/src/lib/types/UserMetadata.ts create mode 100644 frontend/src/routes/user/account/+page.svelte diff --git a/backend/app/auth/router.py b/backend/app/auth/router.py index 8a43db1..48f8727 100644 --- a/backend/app/auth/router.py +++ b/backend/app/auth/router.py @@ -6,13 +6,16 @@ from ..database import get_async_session from ..schemas import RequestStatus +from .dependencies import get_current_user from .exceptions import AUTHENTICATION_2FA_EXCEPTION, CREDENTIALS_EXCEPTION -from .schemas import Code2FA, Token, User +from .models import User as DbUser +from .schemas import Code2FA, Token, User, UserMetadata from .service import ( authenticate_user, create_access_token, create_user, oauth2_scheme, + remove_2fa_code, remove_token, update_2fa_code, verify_2fa_code, @@ -81,3 +84,29 @@ async def get_2fa_token( code_2fa = await update_2fa_code(user, session) return Code2FA(code=code_2fa) + + +@router.post("/2fa/disable") +async def disable_2fa( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()], + session: Annotated[AsyncSession, Depends(get_async_session)], +) -> RequestStatus: + user = await authenticate_user(form_data.username, form_data.password, session) + + if not user: + raise CREDENTIALS_EXCEPTION + + await remove_2fa_code(user, session) + + return RequestStatus(message="2FA disabled successfully") + + +@router.get("/fetch_user_data", response_model=str) +async def fetch_user_data( + current_user: Annotated[DbUser, Depends(get_current_user)], +) -> UserMetadata: + return UserMetadata( + username=str(current_user.username), + quota=int(current_user.quota.value), + is_2fa_enabled=(current_user.code_2fa is not None), + ) diff --git a/backend/app/auth/schemas.py b/backend/app/auth/schemas.py index d2943ef..1b15e58 100644 --- a/backend/app/auth/schemas.py +++ b/backend/app/auth/schemas.py @@ -21,3 +21,9 @@ class TokenData(BaseModel): class Code2FA(BaseModel): code: str + + +class UserMetadata(BaseModel): + username: str + quota: int + is_2fa_enabled: bool diff --git a/backend/app/auth/service.py b/backend/app/auth/service.py index f800912..d640661 100644 --- a/backend/app/auth/service.py +++ b/backend/app/auth/service.py @@ -77,6 +77,11 @@ async def update_2fa_code(user: User, session: AsyncSession) -> str: return code +async def remove_2fa_code(user: User, session: AsyncSession) -> None: + await session.execute(update(User).where(User.id == user.id).values(code_2fa=None)) + await session.commit() + + async def create_access_token( session: AsyncSession, data: dict[str, str | int | datetime], diff --git a/frontend/src/lib/components/Header.svelte b/frontend/src/lib/components/Header.svelte index 93b3bc2..f8ea1f8 100644 --- a/frontend/src/lib/components/Header.svelte +++ b/frontend/src/lib/components/Header.svelte @@ -1,9 +1,19 @@ {#if code == null} -
-

Please log in again to get a new 2FA token

+
+

Please log in again to update the 2FA token


-
+

@@ -53,3 +61,20 @@ height="168" /> {/if} + + diff --git a/frontend/src/routes/user/account/+page.svelte b/frontend/src/routes/user/account/+page.svelte new file mode 100644 index 0000000..7cacffa --- /dev/null +++ b/frontend/src/routes/user/account/+page.svelte @@ -0,0 +1,46 @@ + + +Hi, {username} + +{#if user?.is_2fa_enabled} + + +

Enable 2FA

+
+{:else} + + +

Disable 2FA

+
+ + +

Update 2FA

+
+{/if} diff --git a/frontend/src/server/auth.ts b/frontend/src/server/auth.ts index fdaf3de..d5aaced 100644 --- a/frontend/src/server/auth.ts +++ b/frontend/src/server/auth.ts @@ -1,5 +1,6 @@ import type { AccessToken } from '$lib/types/AccessToken'; import type { Code2FA } from '$lib/types/Code2FA'; +import type { UserMetadata } from '$lib/types/UserMetadata'; import axios from 'axios'; const BASE_URL = import.meta.env.VITE_BASE_URL; @@ -42,3 +43,21 @@ export const get2faToken = async (username: string, password: string) => { return result; }; + +export const remove2faToken = async (username: string, password: string) => { + const formData = new FormData(); + formData.append('username', username); + formData.append('password', password); + const result = await axios.post(`${BASE_URL}/auth/2fa/disable`, formData); + + return result; +}; +export const getUserMetadata = async (token: string) => { + const result = await axios.get(`${BASE_URL}/auth/metadata`, { + headers: { + authorization: `Bearer ${token}` + } + }); + + return result; +}; From 27fa686e87944e15e676131e4437726348a8bfef Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Tue, 11 Jun 2024 02:25:02 +0200 Subject: [PATCH 16/22] update features.md --- docs/FEATURES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 398dc27..fa10c1b 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -8,9 +8,9 @@ - [x] Administration Panel - [x] File Preview - [x] Webhooks -- [ ] Encryption +- [x] Encryption - [x] Protected Files -- [ ] 2FA +- [x] 2FA - [ ] URL Shortening - [ ] Responsiveness - [ ] Localization From 6a52dee3d381e84b79aaadd57c38d4ee5a48eab0 Mon Sep 17 00:00:00 2001 From: Ana Marija Atanasovska Date: Tue, 11 Jun 2024 02:25:48 +0200 Subject: [PATCH 17/22] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5005208..77c97e1 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Authors: - File previews - File whitelist - Encryption +- 2FA - User registration and login - Intuitive interface - Support for webhooks From adbadf2a7e88b86c0e59732a1cbc2b1a4a7b18cb Mon Sep 17 00:00:00 2001 From: Delemangi Date: Thu, 13 Jun 2024 21:32:52 +0200 Subject: [PATCH 18/22] Disable Vitest warning Signed-off-by: Delemangi --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fcb414c..660a6da 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -70,5 +70,6 @@ }, "svelte.enable-ts-plugin": true, "svelte.plugin.svelte.format.config.singleQuote": true, - "svelte.plugin.svelte.format.config.svelteStrictMode": true + "svelte.plugin.svelte.format.config.svelteStrictMode": true, + "vitest.disableWorkspaceWarning": true } From eb96b24beda890e45e1571dd0d70d461a7c852c5 Mon Sep 17 00:00:00 2001 From: Delemangi Date: Thu, 13 Jun 2024 21:51:57 +0200 Subject: [PATCH 19/22] Run Prettier Signed-off-by: Delemangi --- frontend/src/lib/components/Header.svelte | 2 +- frontend/src/lib/components/user/FileRow.svelte | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/Header.svelte b/frontend/src/lib/components/Header.svelte index f8ea1f8..53b1418 100644 --- a/frontend/src/lib/components/Header.svelte +++ b/frontend/src/lib/components/Header.svelte @@ -10,10 +10,10 @@ createStyles, type theme } from '@svelteuidev/core'; + import { Person } from 'radix-icons-svelte'; import { onMount } from 'svelte'; import { writable } from 'svelte/store'; import { clearSession } from '../../auth/session'; - import { Person } from 'radix-icons-svelte'; export let toggleTheme = () => {}; export let currentTheme = 'light'; diff --git a/frontend/src/lib/components/user/FileRow.svelte b/frontend/src/lib/components/user/FileRow.svelte index 2754f25..1f3f0bf 100644 --- a/frontend/src/lib/components/user/FileRow.svelte +++ b/frontend/src/lib/components/user/FileRow.svelte @@ -62,6 +62,7 @@ if (file.encrypted) isDownloadWindowVisible = true; else getFile(); }; + const getFile = async () => { const accessToken = localStorage.getItem('accessToken'); From 08bab467967008347c3824aa1f331d51dfe4df7f Mon Sep 17 00:00:00 2001 From: Delemangi Date: Thu, 13 Jun 2024 21:58:37 +0200 Subject: [PATCH 20/22] Update dependencies Signed-off-by: Delemangi --- backend/poetry.lock | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index a99b473..923fa2d 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiosqlite" @@ -658,18 +658,18 @@ standard = ["fastapi", "uvicorn[standard] (>=0.15.0)"] [[package]] name = "filelock" -version = "3.14.0" +version = "3.15.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, - {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, + {file = "filelock-3.15.1-py3-none-any.whl", hash = "sha256:71b3102950e91dfc1bb4209b64be4dc8854f40e5f534428d8684f953ac847fac"}, + {file = "filelock-3.15.1.tar.gz", hash = "sha256:58a2549afdf9e02e10720eaa4d4470f56386d7a6f72edd7d0596337af8ed7ad8"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] @@ -1405,13 +1405,13 @@ files = [ [[package]] name = "pydantic" -version = "2.7.3" +version = "2.7.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"}, - {file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"}, + {file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"}, + {file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"}, ] [package.dependencies] @@ -1712,6 +1712,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -2449,4 +2450,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11.0" -content-hash = "b1e63a4e0a4d4e6074ae12a599195a4ca0e4481edbff5e9ef76a2318645f21f2" +content-hash = "2861efab85688e7aa1fabc8e305315848790af6472a2fb2a4ed74412cf4d1b45" From fb8265267af4dce8136af9b4fe45956edad9edf3 Mon Sep 17 00:00:00 2001 From: Delemangi Date: Thu, 13 Jun 2024 22:01:48 +0200 Subject: [PATCH 21/22] Fix login link Signed-off-by: Delemangi --- frontend/src/routes/auth/register/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/routes/auth/register/+page.svelte b/frontend/src/routes/auth/register/+page.svelte index 2c41cba..2140f71 100644 --- a/frontend/src/routes/auth/register/+page.svelte +++ b/frontend/src/routes/auth/register/+page.svelte @@ -60,6 +60,6 @@

- Already have an account? Login! + Already have an account? Login!
From 430fc7f9b394db83e1f3d1dec4fd4937e05691b9 Mon Sep 17 00:00:00 2001 From: Delemangi Date: Fri, 14 Jun 2024 01:36:23 +0200 Subject: [PATCH 22/22] Update UI and error handling Signed-off-by: Delemangi --- frontend/src/lib/components/Header.svelte | 2 +- .../src/lib/components/user/FileRow.svelte | 36 +++++++-- frontend/src/routes/auth/login/+page.svelte | 6 +- frontend/src/routes/user/2fa/+page.svelte | 52 ++++++++----- frontend/src/routes/user/account/+page.svelte | 52 ++++++++----- frontend/src/routes/user/home/+page.svelte | 76 +++++++++++++++++-- 6 files changed, 171 insertions(+), 53 deletions(-) diff --git a/frontend/src/lib/components/Header.svelte b/frontend/src/lib/components/Header.svelte index 53b1418..38b9123 100644 --- a/frontend/src/lib/components/Header.svelte +++ b/frontend/src/lib/components/Header.svelte @@ -108,7 +108,7 @@ {/if} {#if accessToken} - + - + + + + + diff --git a/frontend/src/routes/auth/login/+page.svelte b/frontend/src/routes/auth/login/+page.svelte index d02c928..421fca6 100644 --- a/frontend/src/routes/auth/login/+page.svelte +++ b/frontend/src/routes/auth/login/+page.svelte @@ -69,8 +69,6 @@ {/if} {#if code2FAInput} -

2FA authentication enabled

-

Please open your authentication app and enter the code.

@@ -81,4 +79,8 @@

+
+ 2FA Authentication +
+ Please enter the code currently displayed in your authenticator app. {/if} diff --git a/frontend/src/routes/user/2fa/+page.svelte b/frontend/src/routes/user/2fa/+page.svelte index 80d330a..9915b0c 100644 --- a/frontend/src/routes/user/2fa/+page.svelte +++ b/frontend/src/routes/user/2fa/+page.svelte @@ -1,16 +1,16 @@ -Hi, {username} - -{#if user?.is_2fa_enabled} - - -

Enable 2FA

-
-{:else} - - -

Disable 2FA

-
- - -

Update 2FA

-
-{/if} +Hi, {username}! +
+ + + {#if user?.is_2fa_enabled} + + + + Enable 2FA + + + {:else} + + + + Disable 2FA + + + + + + Update 2FA + + + {/if} + diff --git a/frontend/src/routes/user/home/+page.svelte b/frontend/src/routes/user/home/+page.svelte index ff9e116..77cf867 100644 --- a/frontend/src/routes/user/home/+page.svelte +++ b/frontend/src/routes/user/home/+page.svelte @@ -9,6 +9,7 @@ Flex, Overlay, Text, + TextInput, Title, createStyles, type DefaultTheme @@ -104,6 +105,12 @@ alert('An error occurred while fetching the files.'); } }); + + let fileName: string | null = null; + + const handleFileChange = () => { + fileName = filesToUpload?.[0].name ?? null; + };
- {#each userFiles as file} {/each} @@ -130,7 +136,28 @@ Upload File - +
+ + + + + + + {#if filesToUpload} + {fileName} + {:else} + No file selected + {/if} + +
@@ -138,11 +165,15 @@ (filePassword = '')}> - Lock with password? + Encrypt? - {#if passwordLock} - - {/if} +