mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-31 12:15:42 -04:00
refactor: ♻️ rewrite admin CRUD interface for admins (#825)
* docs: 📝 general documentation + add FAQ page * fix(frontend): 🐛 readd missing upload button to backups. * feat(backend): ✨ add support for backup sizes to be displayed on frontend * feat(backend): ✨ add backend for administrator CRUD of users * add admin support for user * refactor(frontend): ♻️ rewrite admin CRUD interface for admins * fix build errors Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
7afdd5b577
commit
dce84c3937
42
docs/docs/documentation/getting-started/faq.md
Normal file
42
docs/docs/documentation/getting-started/faq.md
Normal file
@ -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
|
||||||
|
<iframe width="560" height="315" src="https://www.youtube.com/embed/nAUwKeO93bY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||||
|
```
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
To install Mealie on your server there are a few steps for proper configuration. Let's go through them.
|
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.
|
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.
|
||||||
|
|
||||||
|
@ -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.
|
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
|
!!! 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.
|
In some of the demo gifs the styling may be different than the finale application. demos were done during development prior to finale styling.
|
||||||
|
File diff suppressed because one or more lines are too long
@ -55,7 +55,9 @@ nav:
|
|||||||
- Getting Started:
|
- Getting Started:
|
||||||
- Introduction: "documentation/getting-started/introduction.md"
|
- Introduction: "documentation/getting-started/introduction.md"
|
||||||
- Updating: "documentation/getting-started/updating.md"
|
- Updating: "documentation/getting-started/updating.md"
|
||||||
|
- FAQ: "documentation/getting-started/faq.md"
|
||||||
- API: "documentation/getting-started/api-usage.md"
|
- API: "documentation/getting-started/api-usage.md"
|
||||||
|
|
||||||
- Installation:
|
- Installation:
|
||||||
- Installation Checklist: "documentation/getting-started/installation/installation-checklist.md"
|
- Installation Checklist: "documentation/getting-started/installation/installation-checklist.md"
|
||||||
- SQLite (Recommended): "documentation/getting-started/installation/sqlite.md"
|
- SQLite (Recommended): "documentation/getting-started/installation/sqlite.md"
|
||||||
@ -63,6 +65,7 @@ nav:
|
|||||||
- Frontend Configuration: "documentation/getting-started/installation/frontend-config.md"
|
- Frontend Configuration: "documentation/getting-started/installation/frontend-config.md"
|
||||||
- Backend Configuration: "documentation/getting-started/installation/backend-config.md"
|
- Backend Configuration: "documentation/getting-started/installation/backend-config.md"
|
||||||
- Advanced: "documentation/getting-started/installation/advanced.md"
|
- Advanced: "documentation/getting-started/installation/advanced.md"
|
||||||
|
|
||||||
- Recipes:
|
- Recipes:
|
||||||
- Working With Recipes: "documentation/recipes/recipes.md"
|
- Working With Recipes: "documentation/recipes/recipes.md"
|
||||||
- Organizing Recipes: "documentation/recipes/organizing-recipes.md"
|
- Organizing Recipes: "documentation/recipes/organizing-recipes.md"
|
||||||
@ -88,7 +91,9 @@ nav:
|
|||||||
- Reverse Proxy (SWAG): "documentation/community-guide/swag.md"
|
- Reverse Proxy (SWAG): "documentation/community-guide/swag.md"
|
||||||
- Home Assistant: "documentation/community-guide/home-assistant.md"
|
- Home Assistant: "documentation/community-guide/home-assistant.md"
|
||||||
- Bulk Url Import: "documentation/community-guide/bulk-url-import.md"
|
- Bulk Url Import: "documentation/community-guide/bulk-url-import.md"
|
||||||
|
|
||||||
- API Reference: "api/redoc.md"
|
- API Reference: "api/redoc.md"
|
||||||
|
|
||||||
- Contributors Guide:
|
- Contributors Guide:
|
||||||
- Non-Code: "contributors/non-coders.md"
|
- Non-Code: "contributors/non-coders.md"
|
||||||
- Translating: "contributors/translating.md"
|
- Translating: "contributors/translating.md"
|
||||||
@ -99,7 +104,9 @@ nav:
|
|||||||
- Style Guide: "contributors/developers-guide/style-guide.md"
|
- Style Guide: "contributors/developers-guide/style-guide.md"
|
||||||
- Guides:
|
- Guides:
|
||||||
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
|
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
|
||||||
|
|
||||||
- Development Road Map: "roadmap.md"
|
- Development Road Map: "roadmap.md"
|
||||||
|
|
||||||
- Change Log:
|
- Change Log:
|
||||||
- v1.0.0 A Whole New App: "changelog/v1.0.0.md"
|
- v1.0.0 A Whole New App: "changelog/v1.0.0.md"
|
||||||
- v0.5.2 Misc Updates: "changelog/v0.5.2.md"
|
- v0.5.2 Misc Updates: "changelog/v0.5.2.md"
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { AdminAboutAPI } from "./admin/admin-about";
|
import { AdminAboutAPI } from "./admin/admin-about";
|
||||||
import { AdminTaskAPI } from "./admin/admin-tasks";
|
import { AdminTaskAPI } from "./admin/admin-tasks";
|
||||||
|
import { AdminUsersApi } from "./admin/admin-users";
|
||||||
import { ApiRequestInstance } from "~/types/api";
|
import { ApiRequestInstance } from "~/types/api";
|
||||||
|
|
||||||
export class AdminAPI {
|
export class AdminAPI {
|
||||||
private static instance: AdminAPI;
|
private static instance: AdminAPI;
|
||||||
public about: AdminAboutAPI;
|
public about: AdminAboutAPI;
|
||||||
public serverTasks: AdminTaskAPI;
|
public serverTasks: AdminTaskAPI;
|
||||||
|
public users: AdminUsersApi;
|
||||||
|
|
||||||
constructor(requests: ApiRequestInstance) {
|
constructor(requests: ApiRequestInstance) {
|
||||||
if (AdminAPI.instance instanceof AdminAPI) {
|
if (AdminAPI.instance instanceof AdminAPI) {
|
||||||
@ -14,6 +16,7 @@ export class AdminAPI {
|
|||||||
|
|
||||||
this.about = new AdminAboutAPI(requests);
|
this.about = new AdminAboutAPI(requests);
|
||||||
this.serverTasks = new AdminTaskAPI(requests);
|
this.serverTasks = new AdminTaskAPI(requests);
|
||||||
|
this.users = new AdminUsersApi(requests);
|
||||||
|
|
||||||
Object.freeze(this);
|
Object.freeze(this);
|
||||||
AdminAPI.instance = this;
|
AdminAPI.instance = this;
|
||||||
|
39
frontend/api/admin/admin-users.ts
Normal file
39
frontend/api/admin/admin-users.ts
Normal file
@ -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<UserRead, UserCreate> {
|
||||||
|
baseRoute: string = routes.adminUsers;
|
||||||
|
itemRoute = routes.adminUsersId;
|
||||||
|
}
|
@ -1,288 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="text-center d-print-none">
|
|
||||||
<BaseDialog
|
|
||||||
ref="domImportFromUrlDialog"
|
|
||||||
:title="$t('new-recipe.from-url')"
|
|
||||||
:icon="$globals.icons.link"
|
|
||||||
:submit-text="$t('general.create')"
|
|
||||||
:loading="processing"
|
|
||||||
width="600px"
|
|
||||||
@submit="createOnByUrl"
|
|
||||||
>
|
|
||||||
<v-form ref="domImportFromUrlForm" @submit.prevent="createOnByUrl">
|
|
||||||
<v-card-text>
|
|
||||||
<v-text-field
|
|
||||||
v-model="recipeURL"
|
|
||||||
:label="$t('new-recipe.recipe-url')"
|
|
||||||
validate-on-blur
|
|
||||||
autofocus
|
|
||||||
filled
|
|
||||||
rounded
|
|
||||||
class="rounded-lg"
|
|
||||||
:rules="[validators.url]"
|
|
||||||
:hint="$t('new-recipe.url-form-hint')"
|
|
||||||
persistent-hint
|
|
||||||
></v-text-field>
|
|
||||||
|
|
||||||
<v-expand-transition>
|
|
||||||
<v-alert v-show="error" color="error" class="mt-6 white--text">
|
|
||||||
<v-card-title class="ma-0 pa-0">
|
|
||||||
<v-icon left color="white" x-large> {{ $globals.icons.robot }} </v-icon>
|
|
||||||
{{ $t("new-recipe.error-title") }}
|
|
||||||
</v-card-title>
|
|
||||||
<v-divider class="my-3 mx-2"></v-divider>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{ $t("new-recipe.error-details") }}
|
|
||||||
</p>
|
|
||||||
<div class="d-flex row justify-space-around my-3 force-white">
|
|
||||||
<a
|
|
||||||
class="dark"
|
|
||||||
href="https://developers.google.com/search/docs/data-types/recipe"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer nofollow"
|
|
||||||
>
|
|
||||||
{{ $t("new-recipe.google-ld-json-info") }}
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/hay-kot/mealie/issues" target="_blank" rel="noreferrer nofollow">
|
|
||||||
{{ $t("new-recipe.github-issues") }}
|
|
||||||
</a>
|
|
||||||
<a href="https://schema.org/Recipe" target="_blank" rel="noreferrer nofollow">
|
|
||||||
{{ $t("new-recipe.recipe-markup-specification") }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-end">
|
|
||||||
<v-btn
|
|
||||||
white
|
|
||||||
outlined
|
|
||||||
:to="{ path: '/recipes/debugger', query: { test_url: recipeURL } }"
|
|
||||||
@click="addRecipe = false"
|
|
||||||
>
|
|
||||||
<v-icon left> {{ $globals.icons.externalLink }} </v-icon>
|
|
||||||
{{ $t("new-recipe.view-scraped-data") }}
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</v-alert>
|
|
||||||
</v-expand-transition>
|
|
||||||
</v-card-text>
|
|
||||||
</v-form>
|
|
||||||
</BaseDialog>
|
|
||||||
<BaseDialog
|
|
||||||
ref="domUploadZipDialog"
|
|
||||||
:title="$t('new-recipe.upload-a-recipe')"
|
|
||||||
:icon="$globals.icons.zip"
|
|
||||||
:submit-text="$t('general.import')"
|
|
||||||
:loading="processing"
|
|
||||||
@submit="uploadZip"
|
|
||||||
>
|
|
||||||
<v-card-text class="mt-1 pb-0">
|
|
||||||
{{ $t("new-recipe.upload-individual-zip-file") }}
|
|
||||||
|
|
||||||
<div class="headline mx-auto mb-0 pb-0 text-center">
|
|
||||||
{{ fileName }}
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<v-card-actions>
|
|
||||||
<!-- <AppButtonUpload class="mx-auto" :text-btn="false" :post="false" @uploaded="setFile"> </AppButtonUpload> -->
|
|
||||||
</v-card-actions>
|
|
||||||
</BaseDialog>
|
|
||||||
<BaseDialog
|
|
||||||
ref="domCreateDialog"
|
|
||||||
:icon="$globals.icons.primary"
|
|
||||||
title="Create A Recipe"
|
|
||||||
@submit="manualCreateRecipe()"
|
|
||||||
>
|
|
||||||
<v-card-text class="mt-5">
|
|
||||||
<v-form>
|
|
||||||
<AutoForm v-model="createRecipeData.form" :items="createRecipeData.items" />
|
|
||||||
</v-form>
|
|
||||||
</v-card-text>
|
|
||||||
</BaseDialog>
|
|
||||||
<v-speed-dial v-model="fab" :open-on-hover="absolute" :fixed="absolute" :bottom="absolute" :right="absolute">
|
|
||||||
<template #activator>
|
|
||||||
<v-btn v-model="fab" :color="absolute ? 'accent' : 'white'" dark :icon="!absolute" :fab="absolute">
|
|
||||||
<v-icon> {{ $globals.icons.createAlt }} </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<v-tooltip left dark color="primary">
|
|
||||||
<template #activator="{ on, attrs }">
|
|
||||||
<v-btn fab dark small color="primary" v-bind="attrs" v-on="on" @click="domImportFromUrlDialog.open()">
|
|
||||||
<v-icon>{{ $globals.icons.link }} </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<span>{{ $t("new-recipe.from-url") }}</span>
|
|
||||||
</v-tooltip>
|
|
||||||
<v-tooltip left dark color="accent">
|
|
||||||
<template #activator="{ on, attrs }">
|
|
||||||
<v-btn fab dark small color="accent" v-bind="attrs" v-on="on" @click="domCreateDialog.open()">
|
|
||||||
<v-icon>{{ $globals.icons.edit }}</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<span>{{ $t("general.new") }}</span>
|
|
||||||
</v-tooltip>
|
|
||||||
<v-tooltip left dark color="info">
|
|
||||||
<template #activator="{ on, attrs }">
|
|
||||||
<v-btn fab dark small color="info" v-bind="attrs" v-on="on" @click="domUploadZipDialog.open()">
|
|
||||||
<v-icon>{{ $globals.icons.zip }}</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<span>{{ $t("general.upload") }}</span>
|
|
||||||
</v-tooltip>
|
|
||||||
</v-speed-dial>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
// import AppButtonUpload from "@/components/UI/Buttons/AppButtonUpload.vue";
|
|
||||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
|
||||||
import { fieldTypes } from "~/composables/forms";
|
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
import { validators } from "~/composables/use-validators";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
absolute: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const domCreateDialog = ref(null);
|
|
||||||
const domCreateForm = ref<VForm | null>(null);
|
|
||||||
|
|
||||||
const domUploadZipDialog = ref(null);
|
|
||||||
const domUploadZipForm = ref<VForm | null>(null);
|
|
||||||
|
|
||||||
const domImportFromUrlDialog = ref(null);
|
|
||||||
const domImportFromUrlForm = ref<VForm | null>(null);
|
|
||||||
|
|
||||||
const api = useUserApi();
|
|
||||||
|
|
||||||
return {
|
|
||||||
domCreateDialog,
|
|
||||||
domCreateForm,
|
|
||||||
domUploadZipDialog,
|
|
||||||
domUploadZipForm,
|
|
||||||
domImportFromUrlDialog,
|
|
||||||
domImportFromUrlForm,
|
|
||||||
api,
|
|
||||||
validators,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
error: false,
|
|
||||||
fab: false,
|
|
||||||
addRecipe: false,
|
|
||||||
processing: false,
|
|
||||||
uploadData: {
|
|
||||||
fileName: "archive",
|
|
||||||
file: null,
|
|
||||||
},
|
|
||||||
createRecipeData: {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: "Recipe Name",
|
|
||||||
varName: "name",
|
|
||||||
type: fieldTypes.TEXT,
|
|
||||||
rules: ["required"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
form: {
|
|
||||||
name: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
recipeURL: {
|
|
||||||
set(recipe_import_url: string) {
|
|
||||||
this.$router.replace({ query: { ...this.$route.query, recipe_import_url } });
|
|
||||||
},
|
|
||||||
get(): string | (string | null)[] {
|
|
||||||
return this.$route.query.recipe_import_url || "";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fileName(): string {
|
|
||||||
// @ts-ignore
|
|
||||||
if (this.uploadData?.file?.name) {
|
|
||||||
// @ts-ignore
|
|
||||||
return this.uploadData.file.name;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
if (this.$route.query.recipe_import_url) {
|
|
||||||
this.addRecipe = true;
|
|
||||||
this.createOnByUrl();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
reset() {
|
|
||||||
this.fab = false;
|
|
||||||
this.error = false;
|
|
||||||
this.addRecipe = false;
|
|
||||||
this.recipeURL = "";
|
|
||||||
this.processing = false;
|
|
||||||
},
|
|
||||||
resetVars() {
|
|
||||||
this.uploadData = {
|
|
||||||
fileName: "archive",
|
|
||||||
file: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
setFile(file: any) {
|
|
||||||
this.uploadData.file = file;
|
|
||||||
console.log("Uploaded");
|
|
||||||
},
|
|
||||||
async uploadZip() {
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
formData.append(this.uploadData.fileName, this.uploadData.file);
|
|
||||||
|
|
||||||
const { response, data } = await this.api.upload.file("/api/recipes/create-from-zip", formData);
|
|
||||||
|
|
||||||
if (response && response.status === 201) {
|
|
||||||
// @ts-ignore
|
|
||||||
this.$router.push(`/recipe/${data.slug}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async manualCreateRecipe() {
|
|
||||||
await this.api.recipes.createOne({ name: this.createRecipeData.form.name });
|
|
||||||
},
|
|
||||||
async createOnByUrl() {
|
|
||||||
this.error = false;
|
|
||||||
|
|
||||||
if (this.domImportFromUrlForm?.validate()) {
|
|
||||||
this.processing = true;
|
|
||||||
|
|
||||||
let response;
|
|
||||||
if (typeof this.recipeURL === "string") {
|
|
||||||
response = await this.api.recipes.createOneByUrl(this.recipeURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processing = false;
|
|
||||||
if (response) {
|
|
||||||
this.addRecipe = false;
|
|
||||||
this.recipeURL = "";
|
|
||||||
this.$router.push(`/recipe/${response.data}`);
|
|
||||||
} else {
|
|
||||||
this.error = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-form ref="file">
|
<v-form ref="file">
|
||||||
<input ref="uploader" class="d-none" type="file" @change="onFileChanged" />
|
<input ref="uploader" class="d-none" type="file" :accept="accept" @change="onFileChanged" />
|
||||||
<slot v-bind="{ isSelecting, onButtonClick }">
|
<slot v-bind="{ isSelecting, onButtonClick }">
|
||||||
<v-btn :loading="isSelecting" :small="small" color="accent" :text="textBtn" @click="onButtonClick">
|
<v-btn :loading="isSelecting" :small="small" color="info" :text="textBtn" @click="onButtonClick">
|
||||||
<v-icon left> {{ effIcon }}</v-icon>
|
<v-icon left> {{ effIcon }}</v-icon>
|
||||||
{{ text ? text : defaultText }}
|
{{ text ? text : defaultText }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -43,6 +43,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
accept: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
|
25
frontend/components/global/AppToolbar.vue
Normal file
25
frontend/components/global/AppToolbar.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<v-toolbar flat>
|
||||||
|
<BaseButton color="null" rounded secondary @click="$router.go(-1)">
|
||||||
|
<template #icon> {{ $globals.icons.arrowLeftBold }}</template>
|
||||||
|
Back
|
||||||
|
</BaseButton>
|
||||||
|
<slot></slot>
|
||||||
|
</v-toolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "@nuxtjs/composition-api";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
back: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
@ -18,23 +18,25 @@
|
|||||||
:label="inputField.label"
|
:label="inputField.label"
|
||||||
:name="inputField.varName"
|
:name="inputField.varName"
|
||||||
:hint="inputField.hint || ''"
|
:hint="inputField.hint || ''"
|
||||||
:disabled="updateMode && inputField.fixed"
|
:disabled="updateMode && inputField.disableUpdate"
|
||||||
@change="emitBlur"
|
@change="emitBlur"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Text Field -->
|
<!-- Text Field -->
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-else-if="inputField.type === fieldTypes.TEXT"
|
v-else-if="inputField.type === fieldTypes.TEXT || inputField.type === fieldTypes.PASSWORD"
|
||||||
v-model="value[inputField.varName]"
|
v-model="value[inputField.varName]"
|
||||||
:readonly="inputField.fixed && updateMode"
|
:readonly="inputField.disableUpdate && updateMode"
|
||||||
|
:disabled="inputField.disableUpdate && updateMode"
|
||||||
filled
|
filled
|
||||||
|
:type="inputField.type === fieldTypes.PASSWORD ? 'password' : 'text'"
|
||||||
rounded
|
rounded
|
||||||
class="rounded-lg"
|
class="rounded-lg"
|
||||||
dense
|
dense
|
||||||
:label="inputField.label"
|
:label="inputField.label"
|
||||||
:name="inputField.varName"
|
:name="inputField.varName"
|
||||||
:hint="inputField.hint || ''"
|
:hint="inputField.hint || ''"
|
||||||
:rules="[...rulesByKey(inputField.rules), ...defaultRules]"
|
:rules="!(inputField.disableUpdate && updateMode) ? [...rulesByKey(inputField.rules), ...defaultRules] : []"
|
||||||
lazy-validation
|
lazy-validation
|
||||||
@blur="emitBlur"
|
@blur="emitBlur"
|
||||||
/>
|
/>
|
||||||
@ -43,7 +45,8 @@
|
|||||||
<v-textarea
|
<v-textarea
|
||||||
v-else-if="inputField.type === fieldTypes.TEXT_AREA"
|
v-else-if="inputField.type === fieldTypes.TEXT_AREA"
|
||||||
v-model="value[inputField.varName]"
|
v-model="value[inputField.varName]"
|
||||||
:readonly="inputField.fixed && updateMode"
|
:readonly="inputField.disableUpdate && updateMode"
|
||||||
|
:disabled="inputField.disableUpdate && updateMode"
|
||||||
filled
|
filled
|
||||||
rounded
|
rounded
|
||||||
class="rounded-lg"
|
class="rounded-lg"
|
||||||
@ -62,7 +65,7 @@
|
|||||||
<v-select
|
<v-select
|
||||||
v-else-if="inputField.type === fieldTypes.SELECT"
|
v-else-if="inputField.type === fieldTypes.SELECT"
|
||||||
v-model="value[inputField.varName]"
|
v-model="value[inputField.varName]"
|
||||||
:readonly="inputField.fixed && updateMode"
|
:readonly="inputField.disableUpdate && updateMode"
|
||||||
filled
|
filled
|
||||||
rounded
|
rounded
|
||||||
class="rounded-lg"
|
class="rounded-lg"
|
||||||
|
@ -6,6 +6,5 @@ export const fieldTypes = {
|
|||||||
OBJECT: "object",
|
OBJECT: "object",
|
||||||
BOOLEAN: "boolean",
|
BOOLEAN: "boolean",
|
||||||
COLOR: "color",
|
COLOR: "color",
|
||||||
};
|
PASSWORD: "password",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@ -79,7 +79,6 @@ export const useBackups = function (fetch = true) {
|
|||||||
|
|
||||||
async function deleteBackup() {
|
async function deleteBackup() {
|
||||||
const { response } = await api.backups.deleteOne(deleteTarget.value);
|
const { response } = await api.backups.deleteOne(deleteTarget.value);
|
||||||
|
|
||||||
if (response && response.status === 200) {
|
if (response && response.status === 200) {
|
||||||
refreshBackups();
|
refreshBackups();
|
||||||
}
|
}
|
||||||
|
@ -77,9 +77,9 @@ export const useUser = function (refreshFunc: CallableFunction | null = null) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUser(slug: string, user: UserOut) {
|
async function updateUser(itemId: string, user: UserOut) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const { data } = await api.users.updateOne(slug, user);
|
const { data } = await api.users.updateOne(itemId, user);
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|
||||||
if (refreshFunc) {
|
if (refreshFunc) {
|
||||||
|
1
frontend/composables/use-users/index.ts
Normal file
1
frontend/composables/use-users/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { useUserForm } from "./user-form";
|
68
frontend/composables/use-users/user-form.ts
Normal file
68
frontend/composables/use-users/user-form.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { fieldTypes } from "../forms";
|
||||||
|
import { AutoFormItems } from "~/types/auto-forms";
|
||||||
|
|
||||||
|
export const useUserForm = () => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
@ -76,39 +76,17 @@ export default defineComponent({
|
|||||||
to: "/admin/toolbox/units",
|
to: "/admin/toolbox/units",
|
||||||
title: "Manage 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,
|
icon: $globals.icons.group,
|
||||||
to: "/admin/manage-users",
|
to: "/admin/manage/groups",
|
||||||
title: i18n.t("sidebar.manage-users"),
|
title: i18n.t("group.groups"),
|
||||||
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"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: $globals.icons.database,
|
icon: $globals.icons.database,
|
||||||
|
@ -90,6 +90,16 @@
|
|||||||
</template>
|
</template>
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
<div class="mt-4 d-flex justify-end">
|
||||||
|
<AppButtonUpload
|
||||||
|
:text-btn="false"
|
||||||
|
class="mr-4"
|
||||||
|
url="/api/backups/upload"
|
||||||
|
accept=".zip"
|
||||||
|
color="info"
|
||||||
|
@uploaded="refreshBackups()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
@ -115,6 +125,7 @@ export default defineComponent({
|
|||||||
headers: [
|
headers: [
|
||||||
{ text: i18n.t("general.name"), value: "name" },
|
{ text: i18n.t("general.name"), value: "name" },
|
||||||
{ text: i18n.t("general.created"), value: "date" },
|
{ text: i18n.t("general.created"), value: "date" },
|
||||||
|
{ text: "Size", value: "size" },
|
||||||
{ text: "", value: "actions", align: "right" },
|
{ text: "", value: "actions", align: "right" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="ml-auto">
|
<div class="ml-auto">
|
||||||
<v-btn color="primary" small to="/admin/manage-users/all-users">
|
<v-btn color="primary" small to="/admin/manage/users">
|
||||||
<v-icon left>{{ $globals.icons.user }}</v-icon>
|
<v-icon left>{{ $globals.icons.user }}</v-icon>
|
||||||
{{ $t("user.manage-users") }}
|
{{ $t("user.manage-users") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -65,7 +65,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="ml-auto">
|
<div class="ml-auto">
|
||||||
<v-btn color="primary" small to="/admin/manage-users/all-groups">
|
<v-btn color="primary" small to="/admin/manage/groups">
|
||||||
<v-icon left>{{ $globals.icons.group }}</v-icon>
|
<v-icon left>{{ $globals.icons.group }}</v-icon>
|
||||||
{{ $t("group.manage-groups") }}
|
{{ $t("group.manage-groups") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
@ -1,183 +0,0 @@
|
|||||||
// TODO: Edit User
|
|
||||||
<template>
|
|
||||||
<v-container fluid>
|
|
||||||
<BaseCardSectionTitle title="User Management"> </BaseCardSectionTitle>
|
|
||||||
<section>
|
|
||||||
<v-toolbar color="background" flat class="justify-between">
|
|
||||||
<BaseDialog
|
|
||||||
ref="refUserDialog"
|
|
||||||
top
|
|
||||||
:title="$t('user.create-user')"
|
|
||||||
@submit="createUser(createUserForm.data)"
|
|
||||||
@close="resetForm"
|
|
||||||
>
|
|
||||||
<template #activator="{ open }">
|
|
||||||
<BaseButton @click="open"> {{ $t("user.create-user") }} </BaseButton>
|
|
||||||
</template>
|
|
||||||
<v-card-text>
|
|
||||||
<v-select
|
|
||||||
v-model="createUserForm.data.group"
|
|
||||||
:items="groups"
|
|
||||||
rounded
|
|
||||||
class="rounded-lg"
|
|
||||||
item-text="name"
|
|
||||||
item-value="name"
|
|
||||||
:return-object="false"
|
|
||||||
filled
|
|
||||||
label="Filled style"
|
|
||||||
></v-select>
|
|
||||||
<AutoForm v-model="createUserForm.data" :update-mode="updateMode" :items="createUserForm.items" />
|
|
||||||
</v-card-text>
|
|
||||||
</BaseDialog>
|
|
||||||
</v-toolbar>
|
|
||||||
<v-data-table
|
|
||||||
:headers="headers"
|
|
||||||
:items="users || []"
|
|
||||||
item-key="id"
|
|
||||||
class="elevation-0"
|
|
||||||
hide-default-footer
|
|
||||||
disable-pagination
|
|
||||||
:search="search"
|
|
||||||
>
|
|
||||||
<template #item.admin="{ item }">
|
|
||||||
{{ item.admin ? "Admin" : "User" }}
|
|
||||||
</template>
|
|
||||||
<template #item.actions="{ item }">
|
|
||||||
<BaseDialog :title="$t('general.confirm')" color="error" @confirm="deleteUser(item.id)">
|
|
||||||
<template #activator="{ open }">
|
|
||||||
<v-btn :disabled="item.id == 1" class="mr-1" small color="error" @click="open">
|
|
||||||
<v-icon small left>
|
|
||||||
{{ $globals.icons.delete }}
|
|
||||||
</v-icon>
|
|
||||||
{{ $t("general.delete") }}
|
|
||||||
</v-btn>
|
|
||||||
<v-btn small color="success" @click="updateUser(item)">
|
|
||||||
<v-icon small left class="mr-2">
|
|
||||||
{{ $globals.icons.edit }}
|
|
||||||
</v-icon>
|
|
||||||
{{ $t("general.edit") }}
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<v-card-text>
|
|
||||||
{{ $t("general.confirm-delete-generic") }}
|
|
||||||
</v-card-text>
|
|
||||||
</BaseDialog>
|
|
||||||
</template>
|
|
||||||
</v-data-table>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
</section>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
|
||||||
import { fieldTypes } from "~/composables/forms";
|
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
import { useGroups } from "~/composables/use-groups";
|
|
||||||
import { useUser, useAllUsers } from "~/composables/use-user";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
layout: "admin",
|
|
||||||
setup() {
|
|
||||||
const api = useUserApi();
|
|
||||||
const refUserDialog = ref();
|
|
||||||
|
|
||||||
const { groups } = useGroups();
|
|
||||||
|
|
||||||
const { users, refreshAllUsers } = useAllUsers();
|
|
||||||
const { loading, getUser, deleteUser, createUser } = useUser(refreshAllUsers);
|
|
||||||
|
|
||||||
return { refUserDialog, api, users, deleteUser, createUser, getUser, loading, groups };
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
search: "",
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
text: this.$t("user.user-id"),
|
|
||||||
align: "start",
|
|
||||||
sortable: false,
|
|
||||||
value: "id",
|
|
||||||
},
|
|
||||||
{ text: this.$t("user.username"), value: "username" },
|
|
||||||
{ text: this.$t("user.full-name"), value: "fullName" },
|
|
||||||
{ text: this.$t("user.email"), value: "email" },
|
|
||||||
{ text: this.$t("group.group"), value: "group" },
|
|
||||||
{ text: this.$t("user.admin"), value: "admin" },
|
|
||||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
|
||||||
],
|
|
||||||
updateMode: false,
|
|
||||||
createUserForm: {
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
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: "Passord",
|
|
||||||
varName: "password",
|
|
||||||
fixed: true,
|
|
||||||
type: fieldTypes.TEXT,
|
|
||||||
rules: ["required"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Administrator",
|
|
||||||
varName: "admin",
|
|
||||||
type: fieldTypes.BOOLEAN,
|
|
||||||
rules: ["required"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
data: {
|
|
||||||
username: "",
|
|
||||||
fullName: "",
|
|
||||||
email: "",
|
|
||||||
admin: false,
|
|
||||||
group: "",
|
|
||||||
favoriteRecipes: [],
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
head() {
|
|
||||||
return {
|
|
||||||
title: this.$t("sidebar.manage-users") as string,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateUser(userData: any) {
|
|
||||||
this.updateMode = true;
|
|
||||||
this.createUserForm.data = userData;
|
|
||||||
this.refUserDialog.open();
|
|
||||||
},
|
|
||||||
resetForm() {
|
|
||||||
this.createUserForm.data = {
|
|
||||||
username: "",
|
|
||||||
fullName: "",
|
|
||||||
email: "",
|
|
||||||
admin: false,
|
|
||||||
group: "",
|
|
||||||
favoriteRecipes: [],
|
|
||||||
password: "",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
117
frontend/pages/admin/manage/users/_id.vue
Normal file
117
frontend/pages/admin/manage/users/_id.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<v-container v-if="user" class="narrow-container">
|
||||||
|
<BasePageTitle>
|
||||||
|
<template #header>
|
||||||
|
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-profile.svg')"></v-img>
|
||||||
|
</template>
|
||||||
|
<template #title> Admin User Management </template>
|
||||||
|
Changes to this user will be reflected immediately.
|
||||||
|
</BasePageTitle>
|
||||||
|
<AppToolbar back> </AppToolbar>
|
||||||
|
<v-form v-if="!userError" ref="refNewUserForm" @submit.prevent="handleSubmit">
|
||||||
|
<v-card outlined>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex">
|
||||||
|
<p>User Id: {{ user.id }}</p>
|
||||||
|
</div>
|
||||||
|
<v-select
|
||||||
|
v-if="groups"
|
||||||
|
v-model="user.group"
|
||||||
|
:items="groups"
|
||||||
|
rounded
|
||||||
|
class="rounded-lg"
|
||||||
|
item-text="name"
|
||||||
|
item-value="name"
|
||||||
|
:return-object="false"
|
||||||
|
filled
|
||||||
|
label="User Group"
|
||||||
|
:rules="[validators.required]"
|
||||||
|
></v-select>
|
||||||
|
<AutoForm v-model="user" :items="userForm" update-mode />
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
<div class="d-flex pa-2">
|
||||||
|
<BaseButton type="submit" class="ml-auto"></BaseButton>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, useRoute, onMounted, ref } from "@nuxtjs/composition-api";
|
||||||
|
import { useAdminApi } from "~/composables/api";
|
||||||
|
import { useGroups } from "~/composables/use-groups";
|
||||||
|
import { alert } from "~/composables/use-toast";
|
||||||
|
import { useUserForm } from "~/composables/use-users";
|
||||||
|
import { validators } from "~/composables/use-validators";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
layout: "admin",
|
||||||
|
setup() {
|
||||||
|
const { userForm } = useUserForm();
|
||||||
|
const { groups } = useGroups();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const userId = route.value.params.id;
|
||||||
|
|
||||||
|
// ==============================================
|
||||||
|
// New User Form
|
||||||
|
|
||||||
|
const refNewUserForm = ref<VForm | null>(null);
|
||||||
|
|
||||||
|
const adminApi = useAdminApi();
|
||||||
|
|
||||||
|
const user = ref({
|
||||||
|
username: "",
|
||||||
|
fullName: "",
|
||||||
|
email: "",
|
||||||
|
admin: false,
|
||||||
|
group: "",
|
||||||
|
advanced: false,
|
||||||
|
canInvite: false,
|
||||||
|
canManage: false,
|
||||||
|
canOrganize: false,
|
||||||
|
id: 0,
|
||||||
|
groupId: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userError = ref(false);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const { data, error } = await adminApi.users.getOne(userId);
|
||||||
|
|
||||||
|
if (error?.response?.status === 404) {
|
||||||
|
alert.error("User Not Found");
|
||||||
|
userError.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
// @ts-ignore
|
||||||
|
user.value = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!refNewUserForm.value?.validate()) return;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const { response, data } = await adminApi.users.updateOne(user.value.id, user.value);
|
||||||
|
|
||||||
|
if (response?.status === 200 && data) {
|
||||||
|
// @ts-ignore
|
||||||
|
user.value = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
userError,
|
||||||
|
userForm,
|
||||||
|
refNewUserForm,
|
||||||
|
handleSubmit,
|
||||||
|
groups,
|
||||||
|
validators,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
96
frontend/pages/admin/manage/users/create.vue
Normal file
96
frontend/pages/admin/manage/users/create.vue
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<v-container class="narrow-container">
|
||||||
|
<BasePageTitle class="mb-2">
|
||||||
|
<template #header>
|
||||||
|
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-profile.svg')"></v-img>
|
||||||
|
</template>
|
||||||
|
<template #title> Admin User Creation </template>
|
||||||
|
</BasePageTitle>
|
||||||
|
<AppToolbar back> </AppToolbar>
|
||||||
|
<v-form ref="refNewUserForm" @submit.prevent="handleSubmit">
|
||||||
|
<v-card outlined>
|
||||||
|
<v-card-text>
|
||||||
|
<v-select
|
||||||
|
v-if="groups"
|
||||||
|
v-model="newUserData.group"
|
||||||
|
:items="groups"
|
||||||
|
rounded
|
||||||
|
class="rounded-lg"
|
||||||
|
item-text="name"
|
||||||
|
item-value="name"
|
||||||
|
:return-object="false"
|
||||||
|
filled
|
||||||
|
label="User Group"
|
||||||
|
:rules="[validators.required]"
|
||||||
|
></v-select>
|
||||||
|
<AutoForm v-model="newUserData" :items="userForm" />
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
<div class="d-flex pa-2">
|
||||||
|
<BaseButton type="submit" class="ml-auto"></BaseButton>
|
||||||
|
</div>
|
||||||
|
</v-form>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, useRouter } from "@nuxtjs/composition-api";
|
||||||
|
import { reactive, ref, toRefs } from "vue-demi";
|
||||||
|
import { useAdminApi } from "~/composables/api";
|
||||||
|
import { useGroups } from "~/composables/use-groups";
|
||||||
|
import { useUserForm } from "~/composables/use-users";
|
||||||
|
import { validators } from "~/composables/use-validators";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
layout: "admin",
|
||||||
|
setup() {
|
||||||
|
const { userForm } = useUserForm();
|
||||||
|
const { groups } = useGroups();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// ==============================================
|
||||||
|
// New User Form
|
||||||
|
|
||||||
|
const refNewUserForm = ref<VForm | null>(null);
|
||||||
|
|
||||||
|
const adminApi = useAdminApi();
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
newUserData: {
|
||||||
|
username: "",
|
||||||
|
fullName: "",
|
||||||
|
email: "",
|
||||||
|
admin: false,
|
||||||
|
group: "",
|
||||||
|
advanced: false,
|
||||||
|
canInvite: false,
|
||||||
|
canManage: false,
|
||||||
|
canOrganize: false,
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!refNewUserForm.value?.validate()) return;
|
||||||
|
|
||||||
|
const { response } = await adminApi.users.createOne(state.newUserData);
|
||||||
|
|
||||||
|
if (response?.status === 201) {
|
||||||
|
router.push("/admin/manage/users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
userForm,
|
||||||
|
refNewUserForm,
|
||||||
|
handleSubmit,
|
||||||
|
groups,
|
||||||
|
validators,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
108
frontend/pages/admin/manage/users/index.vue
Normal file
108
frontend/pages/admin/manage/users/index.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// TODO: Edit User
|
||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
<BaseCardSectionTitle title="User Management"> </BaseCardSectionTitle>
|
||||||
|
<section>
|
||||||
|
<v-toolbar color="background" flat class="justify-between">
|
||||||
|
<BaseButton to="/admin/manage/users/create">
|
||||||
|
{{ $t("general.create") }}
|
||||||
|
</BaseButton>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="users || []"
|
||||||
|
item-key="id"
|
||||||
|
class="elevation-0"
|
||||||
|
elevation="0"
|
||||||
|
hide-default-footer
|
||||||
|
disable-pagination
|
||||||
|
:search="search"
|
||||||
|
@click:row="handleRowClick"
|
||||||
|
>
|
||||||
|
<template #item.admin="{ item }">
|
||||||
|
<v-icon right :color="item.admin ? 'success' : null">
|
||||||
|
{{ item.admin ? $globals.icons.checkboxMarkedCircle : $globals.icons.windowClose }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
<template #item.actions="{ item }">
|
||||||
|
<BaseDialog :title="$t('general.confirm')" color="error" @confirm="deleteUser(item.id)">
|
||||||
|
<template #activator="{ open }">
|
||||||
|
<v-btn icon :disabled="item.id == 1" color="error" @click.stop="open">
|
||||||
|
<v-icon>
|
||||||
|
{{ $globals.icons.delete }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-card-text>
|
||||||
|
{{ $t("general.confirm-delete-generic") }}
|
||||||
|
</v-card-text>
|
||||||
|
</BaseDialog>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
</section>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, ref, toRefs, useContext, useRouter } from "@nuxtjs/composition-api";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { useUser, useAllUsers } from "~/composables/use-user";
|
||||||
|
import { UserOut } from "~/types/api-types/user";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
layout: "admin",
|
||||||
|
setup() {
|
||||||
|
const api = useUserApi();
|
||||||
|
const refUserDialog = ref();
|
||||||
|
|
||||||
|
const { i18n } = useContext();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
search: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { users, refreshAllUsers } = useAllUsers();
|
||||||
|
const { loading, deleteUser } = useUser(refreshAllUsers);
|
||||||
|
|
||||||
|
function handleRowClick(item: UserOut) {
|
||||||
|
router.push("/admin/manage/users/" + item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// Constants / Non-reactive
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{
|
||||||
|
text: i18n.t("user.user-id"),
|
||||||
|
align: "start",
|
||||||
|
value: "id",
|
||||||
|
},
|
||||||
|
{ text: i18n.t("user.username"), value: "username" },
|
||||||
|
{ text: i18n.t("user.full-name"), value: "fullName" },
|
||||||
|
{ text: i18n.t("user.email"), value: "email" },
|
||||||
|
{ text: i18n.t("group.group"), value: "group" },
|
||||||
|
{ text: i18n.t("user.admin"), value: "admin" },
|
||||||
|
{ text: i18n.t("general.delete"), value: "actions", sortable: false, align: "center" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state),
|
||||||
|
api,
|
||||||
|
headers,
|
||||||
|
deleteUser,
|
||||||
|
loading,
|
||||||
|
refUserDialog,
|
||||||
|
users,
|
||||||
|
handleRowClick,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("sidebar.manage-users") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
13
frontend/types/auto-forms.ts
Normal file
13
frontend/types/auto-forms.ts
Normal file
@ -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[];
|
76
frontend/types/components.d.ts
vendored
76
frontend/types/components.d.ts
vendored
@ -1,49 +1,45 @@
|
|||||||
// This Code is auto generated by gen_global_componenets.py
|
// This Code is auto generated by gen_global_componenets.py
|
||||||
import BaseCardSectionTitle from "@/components/global/BaseCardSectionTitle.vue";
|
import BaseCardSectionTitle from "@/components/global/BaseCardSectionTitle.vue";
|
||||||
import AppLoader from "@/components/global/AppLoader.vue";
|
import AppLoader from "@/components/global/AppLoader.vue";
|
||||||
import BaseButton from "@/components/global/BaseButton.vue";
|
import BaseButton from "@/components/global/BaseButton.vue";
|
||||||
import BaseDialog from "@/components/global/BaseDialog.vue";
|
import BaseDialog from "@/components/global/BaseDialog.vue";
|
||||||
import BaseStatCard from "@/components/global/BaseStatCard.vue";
|
import BaseStatCard from "@/components/global/BaseStatCard.vue";
|
||||||
import ToggleState from "@/components/global/ToggleState.vue";
|
import ToggleState from "@/components/global/ToggleState.vue";
|
||||||
import AppButtonCopy from "@/components/global/AppButtonCopy.vue";
|
import AppButtonCopy from "@/components/global/AppButtonCopy.vue";
|
||||||
import BaseColorPicker from "@/components/global/BaseColorPicker.vue";
|
import BaseColorPicker from "@/components/global/BaseColorPicker.vue";
|
||||||
import BaseDivider from "@/components/global/BaseDivider.vue";
|
import BaseDivider from "@/components/global/BaseDivider.vue";
|
||||||
import AutoForm from "@/components/global/AutoForm.vue";
|
import AutoForm from "@/components/global/AutoForm.vue";
|
||||||
import AppButtonUpload from "@/components/global/AppButtonUpload.vue";
|
import AppButtonUpload from "@/components/global/AppButtonUpload.vue";
|
||||||
import BasePageTitle from "@/components/global/BasePageTitle.vue";
|
import BasePageTitle from "@/components/global/BasePageTitle.vue";
|
||||||
import BaseAutoForm from "@/components/global/BaseAutoForm.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 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" {
|
declare module "vue" {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
// Global Components
|
// Global Components
|
||||||
BaseCardSectionTitle: typeof BaseCardSectionTitle;
|
BaseCardSectionTitle: typeof BaseCardSectionTitle;
|
||||||
AppLoader: typeof AppLoader;
|
AppLoader: typeof AppLoader;
|
||||||
BaseButton: typeof BaseButton;
|
BaseButton: typeof BaseButton;
|
||||||
BaseDialog: typeof BaseDialog;
|
BaseDialog: typeof BaseDialog;
|
||||||
BaseStatCard: typeof BaseStatCard;
|
BaseStatCard: typeof BaseStatCard;
|
||||||
ToggleState: typeof ToggleState;
|
ToggleState: typeof ToggleState;
|
||||||
AppButtonCopy: typeof AppButtonCopy;
|
AppButtonCopy: typeof AppButtonCopy;
|
||||||
BaseColorPicker: typeof BaseColorPicker;
|
BaseColorPicker: typeof BaseColorPicker;
|
||||||
BaseDivider: typeof BaseDivider;
|
BaseDivider: typeof BaseDivider;
|
||||||
AutoForm: typeof AutoForm;
|
AutoForm: typeof AutoForm;
|
||||||
AppButtonUpload: typeof AppButtonUpload;
|
AppButtonUpload: typeof AppButtonUpload;
|
||||||
BasePageTitle: typeof BasePageTitle;
|
BasePageTitle: typeof BasePageTitle;
|
||||||
BaseAutoForm: typeof BaseAutoForm;
|
BaseAutoForm: typeof BaseAutoForm;
|
||||||
// Layout Components
|
// Layout Components
|
||||||
TheSnackbar: typeof TheSnackbar;
|
TheSnackbar: typeof TheSnackbar;
|
||||||
AppFloatingButton: typeof AppFloatingButton;
|
AppHeader: typeof AppHeader;
|
||||||
AppHeader: typeof AppHeader;
|
AppSidebar: typeof AppSidebar;
|
||||||
AppSidebar: typeof AppSidebar;
|
AppFooter: typeof AppFooter;
|
||||||
AppFooter: typeof AppFooter;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from mealie.routes.routers import AdminAPIRouter
|
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
|
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_about.router, tags=["Admin: About"])
|
||||||
router.include_router(admin_log.router, tags=["Admin: Log"])
|
router.include_router(admin_log.router, tags=["Admin: Log"])
|
||||||
router.include_router(admin_group.router, tags=["Admin: Group"])
|
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_email.router, tags=["Admin: Email"])
|
||||||
router.include_router(admin_server_tasks.router, tags=["Admin: Server Tasks"])
|
router.include_router(admin_server_tasks.router, tags=["Admin: Server Tasks"])
|
||||||
|
@ -15,7 +15,6 @@ router = AdminAPIRouter(prefix="/groups")
|
|||||||
async def get_all_groups(session: Session = Depends(generate_session)):
|
async def get_all_groups(session: Session = Depends(generate_session)):
|
||||||
"""Returns a list of all groups in the database"""
|
"""Returns a list of all groups in the database"""
|
||||||
db = get_database(session)
|
db = get_database(session)
|
||||||
|
|
||||||
return db.groups.get_all()
|
return db.groups.get_all()
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,8 +6,6 @@ from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, s
|
|||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import get_app_dirs
|
from mealie.core.config import get_app_dirs
|
||||||
|
|
||||||
app_dirs = get_app_dirs()
|
|
||||||
from mealie.core.dependencies import get_current_user
|
from mealie.core.dependencies import get_current_user
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.core.security import create_file_token
|
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 import imports
|
||||||
from mealie.services.backups.exports import backup_all
|
from mealie.services.backups.exports import backup_all
|
||||||
from mealie.services.events import create_backup_event
|
from mealie.services.events import create_backup_event
|
||||||
|
from mealie.utils.fs_stats import pretty_size
|
||||||
|
|
||||||
router = AdminAPIRouter(prefix="/api/backups", tags=["Backups"])
|
router = AdminAPIRouter(prefix="/api/backups", tags=["Backups"])
|
||||||
logger = get_logger()
|
logger = get_logger()
|
||||||
|
app_dirs = get_app_dirs()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/available", response_model=AllBackups)
|
@router.get("/available", response_model=AllBackups)
|
||||||
@ -28,7 +28,7 @@ def available_imports():
|
|||||||
"""Returns a list of avaiable .zip files for import into Mealie."""
|
"""Returns a list of avaiable .zip files for import into Mealie."""
|
||||||
imports = []
|
imports = []
|
||||||
for archive in app_dirs.BACKUP_DIR.glob("*.zip"):
|
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)
|
imports.append(backup)
|
||||||
|
|
||||||
templates = [template.name for template in app_dirs.TEMPLATE_DIR.glob("*.*")]
|
templates = [template.name for template in app_dirs.TEMPLATE_DIR.glob("*.*")]
|
||||||
@ -118,3 +118,5 @@ def delete_backup(file_name: str):
|
|||||||
file_path.unlink()
|
file_path.unlink()
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
return {"message": f"{file_name} has been deleted."}
|
||||||
|
@ -60,6 +60,7 @@ class CreateBackup(BaseModel):
|
|||||||
class BackupFile(BaseModel):
|
class BackupFile(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
date: datetime
|
date: datetime
|
||||||
|
size: str
|
||||||
|
|
||||||
|
|
||||||
class AllBackups(BaseModel):
|
class AllBackups(BaseModel):
|
||||||
|
36
mealie/services/admin/admin_user_service.py
Normal file
36
mealie/services/admin/admin_user_service.py
Normal file
@ -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)
|
15
mealie/utils/fs_stats.py
Normal file
15
mealie/utils/fs_stats.py
Normal file
@ -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"
|
Loading…
x
Reference in New Issue
Block a user