diff --git a/docs/docs/documentation/getting-started/faq.md b/docs/docs/documentation/getting-started/faq.md
new file mode 100644
index 000000000000..3cb838264977
--- /dev/null
+++ b/docs/docs/documentation/getting-started/faq.md
@@ -0,0 +1,42 @@
+# Frequently Asked Questions
+
+## How can I change the theme?
+
+You can change the theme by settings the environment variables on the frontend container.
+
+Links:
+
+- [Frontend Theme](/mealie/documentation/getting-started/installation/frontend-config#themeing)
+
+## How can I change the language?
+
+Languages need to be set on the frontend and backend containers as ENV variables.
+
+Links
+
+- [Frontend Config](/mealie/documentation/getting-started/installation/frontend-config/)
+- [Backend Config](/mealie/documentation/getting-started/installation/backend-config/)
+
+## How can I change the Login Session Timeout?
+
+Login session can be configured by setting the `TOKEN_TIME` variable on the backend container.
+
+- [Backend Config](/mealie/documentation/getting-started/installation/backend-config/)
+
+## Can I serve Mealie on a subpath?
+
+No. Due to limitations from the Javascript Framework, mealie doesn't support serving Mealie on a subpath.
+
+## Can I install Mealie without docker?
+
+Yes, you can install Mealie on your local machine. HOWEVER, it is recommended that you don't. Managing non-system versions of python, node, and npm is a pain. Moreover updating and upgrading your system with this configuration is unsupported and will likely require manual interventions. If you insist on installing Mealie on your local machine, you can use the links below to help guide your path.
+
+- [Advanced Installation](/mealie/documentation/getting-started/installation/advanced/)
+
+## How I can attach an Image or Video to a Recipe?
+
+Yes. Mealie's Recipe Steps and other fields support the markdown syntax and therefor supports images and videos. To attach an image to the recipe, you can upload it as an asset and use the provided copy button to generate the html image tag required to render the image. For videos, Mealie provides no way to host videos. You'll need to host your videos with another provider and embed them in your recipe. Generally, the video provider will provide a link to the video and the html tag required to render the video. For example, youtube provides the following link that works inside a step. You can adjust the width and height attributes as necessary to ensure a fit.
+
+```html
+VIDEO
+```
\ No newline at end of file
diff --git a/docs/docs/documentation/getting-started/installation/installation-checklist.md b/docs/docs/documentation/getting-started/installation/installation-checklist.md
index cfef45de850a..cc1f5b0e8f12 100644
--- a/docs/docs/documentation/getting-started/installation/installation-checklist.md
+++ b/docs/docs/documentation/getting-started/installation/installation-checklist.md
@@ -2,7 +2,7 @@
To install Mealie on your server there are a few steps for proper configuration. Let's go through them.
-## Step 0: Pre-work
+## Pre-work
To deploy mealie on your local network it is highly recommended to use docker to deploy the image straight from dockerhub. Using the docker-compose templates provided, you should be able to get a stack up and running easily by changing a few default values and deploying. You can deploy with either SQLite (default) or Postgres. SQLite is sufficient for most use cases. Additionally, with Mealie's automated backup and restore functionality, you can easily move between SQLite and Postgres as you wish.
diff --git a/docs/docs/documentation/getting-started/introduction.md b/docs/docs/documentation/getting-started/introduction.md
index 12c788f98603..cad32e1910c3 100644
--- a/docs/docs/documentation/getting-started/introduction.md
+++ b/docs/docs/documentation/getting-started/introduction.md
@@ -2,7 +2,7 @@
Mealie is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the url and Mealie will automatically import the relevant data or add a family recipe with the UI editor. Mealie also provides an API for interactions from 3rd party applications.
-[Remember to join the Discord](https://discord.gg/QuStdQGSGK)!
+[Remember to join the Discord](https://discord.gg/QuStdQGSGK)
!!! note
In some of the demo gifs the styling may be different than the finale application. demos were done during development prior to finale styling.
diff --git a/docs/docs/overrides/api.html b/docs/docs/overrides/api.html
index c971cd76b2ef..95b6e816e7bc 100644
--- a/docs/docs/overrides/api.html
+++ b/docs/docs/overrides/api.html
@@ -14,7 +14,7 @@
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index aa06c1d145ed..da823f6dc5e4 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -55,7 +55,9 @@ nav:
- Getting Started:
- Introduction: "documentation/getting-started/introduction.md"
- Updating: "documentation/getting-started/updating.md"
+ - FAQ: "documentation/getting-started/faq.md"
- API: "documentation/getting-started/api-usage.md"
+
- Installation:
- Installation Checklist: "documentation/getting-started/installation/installation-checklist.md"
- SQLite (Recommended): "documentation/getting-started/installation/sqlite.md"
@@ -63,6 +65,7 @@ nav:
- Frontend Configuration: "documentation/getting-started/installation/frontend-config.md"
- Backend Configuration: "documentation/getting-started/installation/backend-config.md"
- Advanced: "documentation/getting-started/installation/advanced.md"
+
- Recipes:
- Working With Recipes: "documentation/recipes/recipes.md"
- Organizing Recipes: "documentation/recipes/organizing-recipes.md"
@@ -88,7 +91,9 @@ nav:
- Reverse Proxy (SWAG): "documentation/community-guide/swag.md"
- Home Assistant: "documentation/community-guide/home-assistant.md"
- Bulk Url Import: "documentation/community-guide/bulk-url-import.md"
+
- API Reference: "api/redoc.md"
+
- Contributors Guide:
- Non-Code: "contributors/non-coders.md"
- Translating: "contributors/translating.md"
@@ -99,7 +104,9 @@ nav:
- Style Guide: "contributors/developers-guide/style-guide.md"
- Guides:
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
+
- Development Road Map: "roadmap.md"
+
- Change Log:
- v1.0.0 A Whole New App: "changelog/v1.0.0.md"
- v0.5.2 Misc Updates: "changelog/v0.5.2.md"
diff --git a/frontend/api/admin-api.ts b/frontend/api/admin-api.ts
index d0ee28c42113..951faf5aee63 100644
--- a/frontend/api/admin-api.ts
+++ b/frontend/api/admin-api.ts
@@ -1,11 +1,13 @@
import { AdminAboutAPI } from "./admin/admin-about";
import { AdminTaskAPI } from "./admin/admin-tasks";
+import { AdminUsersApi } from "./admin/admin-users";
import { ApiRequestInstance } from "~/types/api";
export class AdminAPI {
private static instance: AdminAPI;
public about: AdminAboutAPI;
public serverTasks: AdminTaskAPI;
+ public users: AdminUsersApi;
constructor(requests: ApiRequestInstance) {
if (AdminAPI.instance instanceof AdminAPI) {
@@ -14,6 +16,7 @@ export class AdminAPI {
this.about = new AdminAboutAPI(requests);
this.serverTasks = new AdminTaskAPI(requests);
+ this.users = new AdminUsersApi(requests);
Object.freeze(this);
AdminAPI.instance = this;
diff --git a/frontend/api/admin/admin-users.ts b/frontend/api/admin/admin-users.ts
new file mode 100644
index 000000000000..314a383fbd46
--- /dev/null
+++ b/frontend/api/admin/admin-users.ts
@@ -0,0 +1,39 @@
+import { BaseCRUDAPI } from "../_base";
+
+const prefix = "/api";
+
+interface UserCreate {
+ username: string;
+ fullName: string;
+ email: string;
+ admin: boolean;
+ group: string;
+ advanced: boolean;
+ canInvite: boolean;
+ canManage: boolean;
+ canOrganize: boolean;
+ password: string;
+}
+
+export interface UserToken {
+ name: string;
+ id: number;
+ createdAt: Date;
+}
+
+interface UserRead extends UserToken {
+ id: number;
+ groupId: number;
+ favoriteRecipes: any[];
+ tokens: UserToken[];
+}
+
+const routes = {
+ adminUsers: `${prefix}/admin/users`,
+ adminUsersId: (tag: string) => `${prefix}/admin/users/${tag}`,
+};
+
+export class AdminUsersApi extends BaseCRUDAPI {
+ baseRoute: string = routes.adminUsers;
+ itemRoute = routes.adminUsersId;
+}
diff --git a/frontend/components/Layout/AppFloatingButton.vue b/frontend/components/Layout/AppFloatingButton.vue
deleted file mode 100644
index 66a1d135e3d9..000000000000
--- a/frontend/components/Layout/AppFloatingButton.vue
+++ /dev/null
@@ -1,288 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- {{ $globals.icons.robot }}
- {{ $t("new-recipe.error-title") }}
-
-
-
-
- {{ $t("new-recipe.error-details") }}
-
-
-
-
- {{ $globals.icons.externalLink }}
- {{ $t("new-recipe.view-scraped-data") }}
-
-
-
-
-
-
-
-
-
- {{ $t("new-recipe.upload-individual-zip-file") }}
-
-
- {{ fileName }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $globals.icons.createAlt }}
-
-
-
-
-
-
-
- {{ $globals.icons.link }}
-
-
- {{ $t("new-recipe.from-url") }}
-
-
-
-
- {{ $globals.icons.edit }}
-
-
- {{ $t("general.new") }}
-
-
-
-
- {{ $globals.icons.zip }}
-
-
- {{ $t("general.upload") }}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/components/global/AppButtonUpload.vue b/frontend/components/global/AppButtonUpload.vue
index 81fa9ff3d757..cab57c00b061 100644
--- a/frontend/components/global/AppButtonUpload.vue
+++ b/frontend/components/global/AppButtonUpload.vue
@@ -1,8 +1,8 @@
-
+
-
+
{{ effIcon }}
{{ text ? text : defaultText }}
@@ -43,6 +43,10 @@ export default {
type: Boolean,
default: true,
},
+ accept: {
+ type: String,
+ default: "",
+ },
},
setup() {
const api = useUserApi();
diff --git a/frontend/components/global/AppToolbar.vue b/frontend/components/global/AppToolbar.vue
new file mode 100644
index 000000000000..cc69138387c7
--- /dev/null
+++ b/frontend/components/global/AppToolbar.vue
@@ -0,0 +1,25 @@
+
+
+
+ {{ $globals.icons.arrowLeftBold }}
+ Back
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/components/global/AutoForm.vue b/frontend/components/global/AutoForm.vue
index db7fda1973ed..05b14e2ec075 100644
--- a/frontend/components/global/AutoForm.vue
+++ b/frontend/components/global/AutoForm.vue
@@ -18,23 +18,25 @@
:label="inputField.label"
:name="inputField.varName"
:hint="inputField.hint || ''"
- :disabled="updateMode && inputField.fixed"
+ :disabled="updateMode && inputField.disableUpdate"
@change="emitBlur"
/>
@@ -43,7 +45,8 @@
{
+ const userForm: AutoFormItems = [
+ {
+ section: "User Details",
+ label: "User Name",
+ varName: "username",
+ type: fieldTypes.TEXT,
+ rules: ["required"],
+ },
+ {
+ label: "Full Name",
+ varName: "fullName",
+ type: fieldTypes.TEXT,
+ rules: ["required"],
+ },
+ {
+ label: "Email",
+ varName: "email",
+ type: fieldTypes.TEXT,
+ rules: ["required"],
+ },
+ {
+ label: "Password",
+ varName: "password",
+ disableUpdate: true,
+ type: fieldTypes.PASSWORD,
+ rules: ["required"],
+ },
+ {
+ section: "Permissions",
+ label: "Administrator",
+ varName: "admin",
+ type: fieldTypes.BOOLEAN,
+ rules: ["required"],
+ },
+ {
+ label: "User can invite other to group",
+ varName: "canInvite",
+ type: fieldTypes.BOOLEAN,
+ rules: ["required"],
+ },
+ {
+ label: "User can manage group",
+ varName: "canManage",
+ type: fieldTypes.BOOLEAN,
+ rules: ["required"],
+ },
+ {
+ label: "User can organize group data",
+ varName: "canOrganize",
+ type: fieldTypes.BOOLEAN,
+ rules: ["required"],
+ },
+ {
+ label: "Enable advanced features",
+ varName: "advanced",
+ type: fieldTypes.BOOLEAN,
+ rules: ["required"],
+ },
+ ];
+
+ return {
+ userForm,
+ };
+};
diff --git a/frontend/layouts/admin.vue b/frontend/layouts/admin.vue
index b79e346af358..ae950c0eb32d 100644
--- a/frontend/layouts/admin.vue
+++ b/frontend/layouts/admin.vue
@@ -76,39 +76,17 @@ export default defineComponent({
to: "/admin/toolbox/units",
title: "Manage Units",
},
- {
- icon: $globals.icons.tags,
- to: "/admin/toolbox/categories",
- title: i18n.t("sidebar.tags"),
- },
- {
- icon: $globals.icons.tags,
- to: "/admin/toolbox/tags",
- title: i18n.t("sidebar.categories"),
- },
],
},
+ {
+ icon: $globals.icons.user,
+ to: "/admin/manage/users",
+ title: i18n.t("user.users"),
+ },
{
icon: $globals.icons.group,
- to: "/admin/manage-users",
- title: i18n.t("sidebar.manage-users"),
- children: [
- {
- icon: $globals.icons.user,
- to: "/admin/manage-users/all-users",
- title: i18n.t("user.users"),
- },
- {
- icon: $globals.icons.group,
- to: "/admin/manage-users/all-groups",
- title: i18n.t("group.groups"),
- },
- ],
- },
- {
- icon: $globals.icons.import,
- to: "/admin/migrations",
- title: i18n.t("sidebar.migrations"),
+ to: "/admin/manage/groups",
+ title: i18n.t("group.groups"),
},
{
icon: $globals.icons.database,
diff --git a/frontend/pages/admin/backups.vue b/frontend/pages/admin/backups.vue
index 18d7be50ecd2..dcce1567c066 100644
--- a/frontend/pages/admin/backups.vue
+++ b/frontend/pages/admin/backups.vue
@@ -90,6 +90,16 @@
+
@@ -115,6 +125,7 @@ export default defineComponent({
headers: [
{ text: i18n.t("general.name"), value: "name" },
{ text: i18n.t("general.created"), value: "date" },
+ { text: "Size", value: "size" },
{ text: "", value: "actions", align: "right" },
],
});
diff --git a/frontend/pages/admin/dashboard.vue b/frontend/pages/admin/dashboard.vue
index ff15452e56b8..fd75e0e9752f 100644
--- a/frontend/pages/admin/dashboard.vue
+++ b/frontend/pages/admin/dashboard.vue
@@ -42,7 +42,7 @@
-
+
{{ $globals.icons.user }}
{{ $t("user.manage-users") }}
@@ -65,7 +65,7 @@
-
+
{{ $globals.icons.group }}
{{ $t("group.manage-groups") }}
diff --git a/frontend/pages/admin/manage-users/all-users.vue b/frontend/pages/admin/manage-users/all-users.vue
deleted file mode 100644
index df192fb4ff2b..000000000000
--- a/frontend/pages/admin/manage-users/all-users.vue
+++ /dev/null
@@ -1,183 +0,0 @@
-// TODO: Edit User
-
-
-
-
-
-
-
- {{ $t("user.create-user") }}
-
-
-
-
-
-
-
-
-
- {{ item.admin ? "Admin" : "User" }}
-
-
-
-
-
-
- {{ $globals.icons.delete }}
-
- {{ $t("general.delete") }}
-
-
-
- {{ $globals.icons.edit }}
-
- {{ $t("general.edit") }}
-
-
-
- {{ $t("general.confirm-delete-generic") }}
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/frontend/pages/admin/manage-users/all-groups.vue b/frontend/pages/admin/manage/group/index.vue
similarity index 100%
rename from frontend/pages/admin/manage-users/all-groups.vue
rename to frontend/pages/admin/manage/group/index.vue
diff --git a/frontend/pages/admin/manage/users/_id.vue b/frontend/pages/admin/manage/users/_id.vue
new file mode 100644
index 000000000000..67f8a0c15909
--- /dev/null
+++ b/frontend/pages/admin/manage/users/_id.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ Admin User Management
+ Changes to this user will be reflected immediately.
+
+
+
+
+
+
+
User Id: {{ user.id }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/pages/admin/manage/users/create.vue b/frontend/pages/admin/manage/users/create.vue
new file mode 100644
index 000000000000..40b5b1c498b0
--- /dev/null
+++ b/frontend/pages/admin/manage/users/create.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+ Admin User Creation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/pages/admin/manage/users/index.vue b/frontend/pages/admin/manage/users/index.vue
new file mode 100644
index 000000000000..6f3ec714302e
--- /dev/null
+++ b/frontend/pages/admin/manage/users/index.vue
@@ -0,0 +1,108 @@
+// TODO: Edit User
+
+
+
+
+
+
+ {{ $t("general.create") }}
+
+
+
+
+
+ {{ item.admin ? $globals.icons.checkboxMarkedCircle : $globals.icons.windowClose }}
+
+
+
+
+
+
+
+ {{ $globals.icons.delete }}
+
+
+
+
+ {{ $t("general.confirm-delete-generic") }}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/types/auto-forms.ts b/frontend/types/auto-forms.ts
new file mode 100644
index 000000000000..3d787a957a33
--- /dev/null
+++ b/frontend/types/auto-forms.ts
@@ -0,0 +1,13 @@
+type FormFieldType = "text" | "textarea" | "list" | "select" | "object" | "boolean" | "color" | "password";
+
+export interface FormField {
+ section?: string;
+ sectionDetails?: string;
+ label?: string;
+ varName: string;
+ type: FormFieldType;
+ rules?: string[];
+ disableUpdate?: boolean;
+}
+
+export type AutoFormItems = FormField[];
diff --git a/frontend/types/components.d.ts b/frontend/types/components.d.ts
index 8c22f6a1420d..8b4509d6e004 100644
--- a/frontend/types/components.d.ts
+++ b/frontend/types/components.d.ts
@@ -1,49 +1,45 @@
// This Code is auto generated by gen_global_componenets.py
- import BaseCardSectionTitle from "@/components/global/BaseCardSectionTitle.vue";
- import AppLoader from "@/components/global/AppLoader.vue";
- import BaseButton from "@/components/global/BaseButton.vue";
- import BaseDialog from "@/components/global/BaseDialog.vue";
- import BaseStatCard from "@/components/global/BaseStatCard.vue";
- import ToggleState from "@/components/global/ToggleState.vue";
- import AppButtonCopy from "@/components/global/AppButtonCopy.vue";
- import BaseColorPicker from "@/components/global/BaseColorPicker.vue";
- import BaseDivider from "@/components/global/BaseDivider.vue";
- import AutoForm from "@/components/global/AutoForm.vue";
- import AppButtonUpload from "@/components/global/AppButtonUpload.vue";
- import BasePageTitle from "@/components/global/BasePageTitle.vue";
- import BaseAutoForm from "@/components/global/BaseAutoForm.vue";
-
- import TheSnackbar from "@/components/layout/TheSnackbar.vue";
- import AppFloatingButton from "@/components/layout/AppFloatingButton.vue";
- import AppHeader from "@/components/layout/AppHeader.vue";
- import AppSidebar from "@/components/layout/AppSidebar.vue";
- import AppFooter from "@/components/layout/AppFooter.vue";
+import BaseCardSectionTitle from "@/components/global/BaseCardSectionTitle.vue";
+import AppLoader from "@/components/global/AppLoader.vue";
+import BaseButton from "@/components/global/BaseButton.vue";
+import BaseDialog from "@/components/global/BaseDialog.vue";
+import BaseStatCard from "@/components/global/BaseStatCard.vue";
+import ToggleState from "@/components/global/ToggleState.vue";
+import AppButtonCopy from "@/components/global/AppButtonCopy.vue";
+import BaseColorPicker from "@/components/global/BaseColorPicker.vue";
+import BaseDivider from "@/components/global/BaseDivider.vue";
+import AutoForm from "@/components/global/AutoForm.vue";
+import AppButtonUpload from "@/components/global/AppButtonUpload.vue";
+import BasePageTitle from "@/components/global/BasePageTitle.vue";
+import BaseAutoForm from "@/components/global/BaseAutoForm.vue";
+import TheSnackbar from "@/components/layout/TheSnackbar.vue";
+import AppHeader from "@/components/layout/AppHeader.vue";
+import AppSidebar from "@/components/layout/AppSidebar.vue";
+import AppFooter from "@/components/layout/AppFooter.vue";
declare module "vue" {
export interface GlobalComponents {
// Global Components
- BaseCardSectionTitle: typeof BaseCardSectionTitle;
- AppLoader: typeof AppLoader;
- BaseButton: typeof BaseButton;
- BaseDialog: typeof BaseDialog;
- BaseStatCard: typeof BaseStatCard;
- ToggleState: typeof ToggleState;
- AppButtonCopy: typeof AppButtonCopy;
- BaseColorPicker: typeof BaseColorPicker;
- BaseDivider: typeof BaseDivider;
- AutoForm: typeof AutoForm;
- AppButtonUpload: typeof AppButtonUpload;
- BasePageTitle: typeof BasePageTitle;
- BaseAutoForm: typeof BaseAutoForm;
- // Layout Components
- TheSnackbar: typeof TheSnackbar;
- AppFloatingButton: typeof AppFloatingButton;
- AppHeader: typeof AppHeader;
- AppSidebar: typeof AppSidebar;
- AppFooter: typeof AppFooter;
-
+ BaseCardSectionTitle: typeof BaseCardSectionTitle;
+ AppLoader: typeof AppLoader;
+ BaseButton: typeof BaseButton;
+ BaseDialog: typeof BaseDialog;
+ BaseStatCard: typeof BaseStatCard;
+ ToggleState: typeof ToggleState;
+ AppButtonCopy: typeof AppButtonCopy;
+ BaseColorPicker: typeof BaseColorPicker;
+ BaseDivider: typeof BaseDivider;
+ AutoForm: typeof AutoForm;
+ AppButtonUpload: typeof AppButtonUpload;
+ BasePageTitle: typeof BasePageTitle;
+ BaseAutoForm: typeof BaseAutoForm;
+ // Layout Components
+ TheSnackbar: typeof TheSnackbar;
+ AppHeader: typeof AppHeader;
+ AppSidebar: typeof AppSidebar;
+ AppFooter: typeof AppFooter;
}
}
-export {};
\ No newline at end of file
+export {};
diff --git a/mealie/routes/admin/__init__.py b/mealie/routes/admin/__init__.py
index 9aa78c47e23d..68ea20d7ad3d 100644
--- a/mealie/routes/admin/__init__.py
+++ b/mealie/routes/admin/__init__.py
@@ -1,6 +1,8 @@
from fastapi import APIRouter
from mealie.routes.routers import AdminAPIRouter
+from mealie.services._base_http_service.router_factory import RouterFactory
+from mealie.services.admin.admin_user_service import AdminUserService
from . import admin_about, admin_email, admin_group, admin_log, admin_server_tasks
@@ -9,5 +11,6 @@ router = AdminAPIRouter(prefix="/admin")
router.include_router(admin_about.router, tags=["Admin: About"])
router.include_router(admin_log.router, tags=["Admin: Log"])
router.include_router(admin_group.router, tags=["Admin: Group"])
+router.include_router(RouterFactory(AdminUserService, prefix="/users", tags=["Admin: Users"]))
router.include_router(admin_email.router, tags=["Admin: Email"])
router.include_router(admin_server_tasks.router, tags=["Admin: Server Tasks"])
diff --git a/mealie/routes/admin/admin_group.py b/mealie/routes/admin/admin_group.py
index 9946278a94c5..927b9cab126c 100644
--- a/mealie/routes/admin/admin_group.py
+++ b/mealie/routes/admin/admin_group.py
@@ -15,7 +15,6 @@ router = AdminAPIRouter(prefix="/groups")
async def get_all_groups(session: Session = Depends(generate_session)):
"""Returns a list of all groups in the database"""
db = get_database(session)
-
return db.groups.get_all()
diff --git a/mealie/routes/backup_routes.py b/mealie/routes/backup_routes.py
index 55ddf5fc1d33..d12e7a26659b 100644
--- a/mealie/routes/backup_routes.py
+++ b/mealie/routes/backup_routes.py
@@ -6,8 +6,6 @@ from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, s
from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_dirs
-
-app_dirs = get_app_dirs()
from mealie.core.dependencies import get_current_user
from mealie.core.root_logger import get_logger
from mealie.core.security import create_file_token
@@ -18,9 +16,11 @@ from mealie.schema.user.user import PrivateUser
from mealie.services.backups import imports
from mealie.services.backups.exports import backup_all
from mealie.services.events import create_backup_event
+from mealie.utils.fs_stats import pretty_size
router = AdminAPIRouter(prefix="/api/backups", tags=["Backups"])
logger = get_logger()
+app_dirs = get_app_dirs()
@router.get("/available", response_model=AllBackups)
@@ -28,7 +28,7 @@ def available_imports():
"""Returns a list of avaiable .zip files for import into Mealie."""
imports = []
for archive in app_dirs.BACKUP_DIR.glob("*.zip"):
- backup = BackupFile(name=archive.name, date=archive.stat().st_ctime)
+ backup = BackupFile(name=archive.name, date=archive.stat().st_ctime, size=pretty_size(archive.stat().st_size))
imports.append(backup)
templates = [template.name for template in app_dirs.TEMPLATE_DIR.glob("*.*")]
@@ -118,3 +118,5 @@ def delete_backup(file_name: str):
file_path.unlink()
except Exception:
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ return {"message": f"{file_name} has been deleted."}
diff --git a/mealie/schema/admin/backup.py b/mealie/schema/admin/backup.py
index 3b557e8fc11c..2823410af282 100644
--- a/mealie/schema/admin/backup.py
+++ b/mealie/schema/admin/backup.py
@@ -60,6 +60,7 @@ class CreateBackup(BaseModel):
class BackupFile(BaseModel):
name: str
date: datetime
+ size: str
class AllBackups(BaseModel):
diff --git a/mealie/services/admin/admin_user_service.py b/mealie/services/admin/admin_user_service.py
new file mode 100644
index 000000000000..7963898e79e4
--- /dev/null
+++ b/mealie/services/admin/admin_user_service.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+from functools import cached_property
+
+from mealie.schema.user.user import UserIn, UserOut
+from mealie.services._base_http_service.crud_http_mixins import CrudHttpMixins
+from mealie.services._base_http_service.http_services import AdminHttpService
+from mealie.services.events import create_recipe_event
+
+
+class AdminUserService(
+ CrudHttpMixins[UserOut, UserIn, UserIn],
+ AdminHttpService[int, UserOut],
+):
+ event_func = create_recipe_event
+ _schema = UserOut
+
+ @cached_property
+ def dal(self):
+ return self.db.users
+
+ def populate_item(self, id: int) -> UserOut:
+ self.item = self.dal.get_one(id)
+ return self.item
+
+ def get_all(self) -> list[UserOut]:
+ return self.dal.get_all()
+
+ def create_one(self, data: UserIn) -> UserOut:
+ return self._create_one(data)
+
+ def update_one(self, data: UserOut, item_id: int = None) -> UserOut:
+ return self._update_one(data, item_id)
+
+ def delete_one(self, id: int = None) -> UserOut:
+ return self._delete_one(id)
diff --git a/mealie/utils/fs_stats.py b/mealie/utils/fs_stats.py
new file mode 100644
index 000000000000..b114db776207
--- /dev/null
+++ b/mealie/utils/fs_stats.py
@@ -0,0 +1,15 @@
+def pretty_size(size: int) -> str:
+ """
+ Pretty size takes in a integer value of a file size and returns the most applicable
+ file unit and the size.
+ """
+ if size < 1024:
+ return f"{size} bytes"
+ elif size < 1024 ** 2:
+ return f"{round(size / 1024, 2)} KB"
+ elif size < 1024 ** 2 * 1024:
+ return f"{round(size / 1024 / 1024, 2)} MB"
+ elif size < 1024 ** 2 * 1024 * 1024:
+ return f"{round(size / 1024 / 1024 / 1024, 2)} GB"
+ else:
+ return f"{round(size / 1024 / 1024 / 1024 / 1024, 2)} TB"