diff --git a/frontend/src/api/api-utils.js b/frontend/src/api/api-utils.js
index 8c4b0e71d..853d5ac9f 100644
--- a/frontend/src/api/api-utils.js
+++ b/frontend/src/api/api-utils.js
@@ -61,9 +61,16 @@ const apiReq = {
processResponse(response);
return response;
},
+
+ async download(url) {
+ const response = await this.get(url);
+ const token = response.data.fileToken;
+
+ const tokenURL = baseURL + "utils/download?token=" + token;
+ window.open(tokenURL, "_blank");
+ return response.data;
+ },
};
-
-
export { apiReq };
export { baseURL };
diff --git a/frontend/src/api/backup.js b/frontend/src/api/backup.js
index 3ec9a1285..b6afd6e98 100644
--- a/frontend/src/api/backup.js
+++ b/frontend/src/api/backup.js
@@ -4,7 +4,7 @@ import { store } from "@/store";
const backupBase = baseURL + "backups/";
-const backupURLs = {
+export const backupURLs = {
// Backup
available: `${backupBase}available`,
createBackup: `${backupBase}export/database`,
@@ -13,6 +13,8 @@ const backupURLs = {
downloadBackup: fileName => `${backupBase}${fileName}/download`,
};
+
+
export const backupAPI = {
/**
* Request all backups available on the server
@@ -43,19 +45,19 @@ export const backupAPI = {
/**
* Creates a backup on the serve given a set of options
* @param {object} data
- * @returns
+ * @returns
*/
async create(options) {
let response = apiReq.post(backupURLs.createBackup, options);
return response;
},
/**
- * Downloads a file from the server. I don't actually think this is used?
- * @param {string} fileName
+ * Downloads a file from the server. I don't actually think this is used?
+ * @param {string} fileName
* @returns Download URL
*/
async download(fileName) {
- let response = await apiReq.get(backupURLs.downloadBackup(fileName));
- return response.data;
+ const url = backupURLs.downloadBackup(fileName);
+ apiReq.download(url);
},
};
diff --git a/frontend/src/components/Admin/Backup/ImportDialog.vue b/frontend/src/components/Admin/Backup/ImportDialog.vue
index 0d54aeaa5..b379dd982 100644
--- a/frontend/src/components/Admin/Backup/ImportDialog.vue
+++ b/frontend/src/components/Admin/Backup/ImportDialog.vue
@@ -37,14 +37,7 @@
-
- {{ $t("general.download") }}
-
+
{{ $t("general.delete") }}
@@ -66,9 +59,10 @@
diff --git a/frontend/src/components/UI/TheDownloadBtn.vue b/frontend/src/components/UI/TheDownloadBtn.vue
new file mode 100644
index 000000000..bc13898d6
--- /dev/null
+++ b/frontend/src/components/UI/TheDownloadBtn.vue
@@ -0,0 +1,51 @@
+
+
+ {{ showButtonText }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mealie/app.py b/mealie/app.py
index 443d8780a..0d16c615b 100644
--- a/mealie/app.py
+++ b/mealie/app.py
@@ -5,7 +5,7 @@ from mealie.core import root_logger
# import utils.startup as startup
from mealie.core.config import APP_VERSION, settings
-from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes
+from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes, utility_routes
from mealie.routes.groups import groups
from mealie.routes.mealplans import mealplans
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes
@@ -29,6 +29,7 @@ def start_scheduler():
def api_routers():
# Authentication
+ app.include_router(utility_routes.router)
app.include_router(users.router)
app.include_router(groups.router)
# Recipes
@@ -36,7 +37,6 @@ def api_routers():
app.include_router(category_routes.router)
app.include_router(tag_routes.router)
app.include_router(recipe_crud_routes.router)
-
# Meal Routes
app.include_router(mealplans.router)
# Settings Routes
diff --git a/mealie/core/security.py b/mealie/core/security.py
index e7a0c8c6a..75758e60c 100644
--- a/mealie/core/security.py
+++ b/mealie/core/security.py
@@ -1,9 +1,10 @@
from datetime import datetime, timedelta
-from mealie.schema.user import UserInDB
+from pathlib import Path
from jose import jwt
from mealie.core.config import settings
from mealie.db.database import db
+from mealie.schema.user import UserInDB
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@@ -20,6 +21,11 @@ def create_access_token(data: dict(), expires_delta: timedelta = None) -> str:
return jwt.encode(to_encode, settings.SECRET, algorithm=ALGORITHM)
+def create_file_token(file_path: Path) -> bool:
+ token_data = {"file": str(file_path)}
+ return create_access_token(token_data, expires_delta=timedelta(minutes=30))
+
+
def authenticate_user(session, email: str, password: str) -> UserInDB:
user: UserInDB = db.users.get(session, email, "email")
if not user:
diff --git a/mealie/routes/backup_routes.py b/mealie/routes/backup_routes.py
index da1dac21b..6b41c444e 100644
--- a/mealie/routes/backup_routes.py
+++ b/mealie/routes/backup_routes.py
@@ -1,10 +1,12 @@
import operator
import shutil
+from typing import Optional
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from mealie.core.config import app_dirs
+from mealie.core.security import create_file_token
from mealie.db.db_setup import generate_session
-from mealie.routes.deps import get_current_user
+from mealie.routes.deps import get_current_user, validate_file_token
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
from mealie.schema.snackbar import SnackResponse
from mealie.services.backups import imports
@@ -68,13 +70,10 @@ def upload_backup_file(archive: UploadFile = File(...)):
@router.get("/{file_name}/download")
async def download_backup_file(file_name: str):
- """ Upload a .zip File to later be imported into Mealie """
+ """ Returns a token to download a file """
file = app_dirs.BACKUP_DIR.joinpath(file_name)
- if file.is_file:
- return FileResponse(file, media_type="application/octet-stream", filename=file_name)
- else:
- return SnackResponse.error("No File Found")
+ return {"fileToken": create_file_token(file)}
@router.post("/{file_name}/import", status_code=200)
diff --git a/mealie/routes/debug_routes.py b/mealie/routes/debug_routes.py
index ab062026d..8dc919683 100644
--- a/mealie/routes/debug_routes.py
+++ b/mealie/routes/debug_routes.py
@@ -3,6 +3,7 @@ import json
from fastapi import APIRouter, Depends
from mealie.core.config import APP_VERSION, app_dirs, settings
from mealie.core.root_logger import LOGGER_FILE
+from mealie.core.security import create_file_token
from mealie.routes.deps import get_current_user
from mealie.schema.debug import AppInfo, DebugInfo
@@ -37,10 +38,8 @@ async def get_mealie_version():
@router.get("/last-recipe-json")
async def get_last_recipe_json(current_user=Depends(get_current_user)):
- """ Doc Str """
-
- with open(app_dirs.DEBUG_DIR.joinpath("last_recipe.json"), "r") as f:
- return json.loads(f.read())
+ """ Returns a token to download a file """
+ return {"fileToken": create_file_token(app_dirs.DEBUG_DIR.joinpath("last_recipe.json"))}
@router.get("/log/{num}")
@@ -51,6 +50,12 @@ async def get_log(num: int, current_user=Depends(get_current_user)):
return log_text
+@router.get("/log")
+async def get_log_file():
+ """ Returns a token to download a file """
+ return {"fileToken": create_file_token(LOGGER_FILE)}
+
+
def tail(f, lines=20):
total_lines_wanted = lines
diff --git a/mealie/routes/deps.py b/mealie/routes/deps.py
index a5cd3a65b..c7d308f57 100644
--- a/mealie/routes/deps.py
+++ b/mealie/routes/deps.py
@@ -1,3 +1,6 @@
+from pathlib import Path
+from typing import Optional
+
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
@@ -25,7 +28,25 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
+
user = db.users.get(session, token_data.username, "email")
if user is None:
raise credentials_exception
return user
+
+
+async def validate_file_token(token: Optional[str] = None) -> Path:
+ credentials_exception = HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="could not validate file token",
+ )
+ if not token:
+ return None
+
+ try:
+ payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
+ file_path = Path(payload.get("file"))
+ except JWTError:
+ raise credentials_exception
+
+ return file_path
diff --git a/mealie/routes/utility_routes.py b/mealie/routes/utility_routes.py
new file mode 100644
index 000000000..c719b4729
--- /dev/null
+++ b/mealie/routes/utility_routes.py
@@ -0,0 +1,20 @@
+from pathlib import Path
+from typing import Optional
+
+from fastapi import APIRouter, Depends
+from mealie.routes.deps import validate_file_token
+from mealie.schema.snackbar import SnackResponse
+from starlette.responses import FileResponse
+
+router = APIRouter(prefix="/api/utils", tags=["Utils"], include_in_schema=True)
+
+
+@router.get("/download")
+async def download_file(file_path: Optional[Path] = Depends(validate_file_token)):
+ """ Uses a file token obtained by an active user to retrieve a file from the operating
+ system. """
+ print("File Name:", file_path)
+ if file_path.is_file():
+ return FileResponse(file_path, media_type="application/octet-stream", filename=file_path.name)
+ else:
+ return SnackResponse.error("No File Found")