mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
Merge remote-tracking branch 'upstream/dev' into localized-meal-date
This commit is contained in:
commit
62bfbbd26a
122 changed files with 1909 additions and 695 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -153,3 +153,4 @@ node_modules/
|
|||
mealie/data/debug/last_recipe.json
|
||||
*.sqlite
|
||||
app_data/db/test.db
|
||||
scratch.py
|
||||
|
|
|
@ -27,12 +27,12 @@
|
|||
<v-icon>mdi-magnify</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<Menu />
|
||||
<SiteMenu />
|
||||
<LanguageMenu />
|
||||
</v-app-bar>
|
||||
<v-main>
|
||||
<v-container>
|
||||
<AddRecipeFab />
|
||||
<SnackBar />
|
||||
<router-view></router-view>
|
||||
</v-container>
|
||||
<FlashMessage :position="'right bottom'"></FlashMessage>
|
||||
|
@ -41,19 +41,20 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Menu from "./components/UI/Menu";
|
||||
import SearchBar from "./components/UI/SearchBar";
|
||||
import AddRecipeFab from "./components/UI/AddRecipeFab";
|
||||
import SnackBar from "./components/UI/SnackBar";
|
||||
import SiteMenu from "@/components/UI/SiteMenu";
|
||||
import SearchBar from "@/components/UI/Search/SearchBar";
|
||||
import AddRecipeFab from "@/components/UI/AddRecipeFab";
|
||||
import LanguageMenu from "@/components/UI/LanguageMenu";
|
||||
import Vuetify from "./plugins/vuetify";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
|
||||
components: {
|
||||
Menu,
|
||||
SiteMenu,
|
||||
AddRecipeFab,
|
||||
SnackBar,
|
||||
SearchBar,
|
||||
LanguageMenu,
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
@ -63,7 +64,7 @@ export default {
|
|||
},
|
||||
created() {
|
||||
window.addEventListener("keyup", e => {
|
||||
if (e.key == "/" && !document.activeElement.id.startsWith('input') ) {
|
||||
if (e.key == "/" && !document.activeElement.id.startsWith("input")) {
|
||||
this.search = !this.search;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import backup from "./api/backup";
|
||||
import recipe from "./api/recipe";
|
||||
import mealplan from "./api/mealplan";
|
||||
import settings from "./api/settings";
|
||||
import themes from "./api/themes";
|
||||
import migration from "./api/migration";
|
||||
import myUtils from "./api/upload";
|
||||
import category from "./api/category";
|
||||
import meta from "./api/meta";
|
||||
|
||||
// import api from "@/api";
|
||||
|
||||
export default {
|
||||
recipes: recipe,
|
||||
backups: backup,
|
||||
mealPlans: mealplan,
|
||||
settings: settings,
|
||||
themes: themes,
|
||||
migrations: migration,
|
||||
utils: myUtils,
|
||||
categories: category,
|
||||
meta: meta,
|
||||
};
|
|
@ -1,6 +1,11 @@
|
|||
const baseURL = "/api/";
|
||||
import axios from "axios";
|
||||
import utils from "@/utils";
|
||||
import { store } from "../store/store";
|
||||
|
||||
axios.defaults.headers.common[
|
||||
"Authorization"
|
||||
] = `Bearer ${store.getters.getToken}`;
|
||||
|
||||
function processResponse(response) {
|
||||
try {
|
||||
|
|
24
frontend/src/api/index.js
Normal file
24
frontend/src/api/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import backup from "./backup";
|
||||
import recipe from "./recipe";
|
||||
import mealplan from "./mealplan";
|
||||
import settings from "./settings";
|
||||
import themes from "./themes";
|
||||
import migration from "./migration";
|
||||
import myUtils from "./upload";
|
||||
import category from "./category";
|
||||
import meta from "./meta";
|
||||
import users from "./users"
|
||||
|
||||
|
||||
export default {
|
||||
recipes: recipe,
|
||||
backups: backup,
|
||||
mealPlans: mealplan,
|
||||
settings: settings,
|
||||
themes: themes,
|
||||
migrations: migration,
|
||||
utils: myUtils,
|
||||
categories: category,
|
||||
meta: meta,
|
||||
users: users
|
||||
};
|
|
@ -70,7 +70,7 @@ export default {
|
|||
router.push(`/`);
|
||||
},
|
||||
|
||||
async allByKeys(recipeKeys, num = 100) {
|
||||
async allByKeys(recipeKeys, num = 999) {
|
||||
const response = await apiReq.get(recipeURLs.allRecipes, {
|
||||
params: {
|
||||
keys: recipeKeys,
|
||||
|
|
|
@ -33,7 +33,6 @@ export default {
|
|||
colors: colors,
|
||||
};
|
||||
let response = await apiReq.put(settingsURLs.updateTheme(themeName), body);
|
||||
console.log(response.data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
|
49
frontend/src/api/users.js
Normal file
49
frontend/src/api/users.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
const authPrefix = baseURL + "auth";
|
||||
const userPrefix = baseURL + "users";
|
||||
|
||||
const authURLs = {
|
||||
token: `${authPrefix}/token`,
|
||||
};
|
||||
|
||||
const usersURLs = {
|
||||
users: `${userPrefix}`,
|
||||
self: `${userPrefix}/self`,
|
||||
userID: id => `${userPrefix}/${id}`,
|
||||
};
|
||||
|
||||
export default {
|
||||
async login(formData) {
|
||||
let response = await apiReq.post(authURLs.token, formData, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
return response;
|
||||
},
|
||||
async allUsers() {
|
||||
let response = await apiReq.get(usersURLs.users);
|
||||
return response.data;
|
||||
},
|
||||
async create(user) {
|
||||
let response = await apiReq.post(usersURLs.users, user);
|
||||
return response.data;
|
||||
},
|
||||
async self() {
|
||||
let response = await apiReq.get(usersURLs.self);
|
||||
return response.data;
|
||||
},
|
||||
async byID(id) {
|
||||
let response = await apiReq.get(usersURLs.userID(id));
|
||||
return response.data;
|
||||
},
|
||||
async update(user) {
|
||||
let response = await apiReq.put(usersURLs.userID(user.id), user);
|
||||
return response.data;
|
||||
},
|
||||
async delete(id) {
|
||||
let response = await apiReq.delete(usersURLs.userID(id));
|
||||
return response.data;
|
||||
},
|
||||
};
|
139
frontend/src/components/Admin/AdminSidebar.vue
Normal file
139
frontend/src/components/Admin/AdminSidebar.vue
Normal file
|
@ -0,0 +1,139 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-btn
|
||||
class="mt-9 ml-n1"
|
||||
fixed
|
||||
left
|
||||
bottom
|
||||
fab
|
||||
small
|
||||
color="primary"
|
||||
@click="showSidebar = !showSidebar"
|
||||
>
|
||||
<v-icon>mdi-cog</v-icon></v-btn
|
||||
>
|
||||
|
||||
<v-navigation-drawer
|
||||
:value="mobile ? showSidebar : true"
|
||||
v-model="showSidebar"
|
||||
width="180px"
|
||||
clipped
|
||||
app
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-list-item two-line>
|
||||
<v-list-item-avatar>
|
||||
<img src="https://randomuser.me/api/portraits/women/81.jpg" />
|
||||
</v-list-item-avatar>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Jane Smith</v-list-item-title>
|
||||
<v-list-item-subtitle>Admin</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list nav dense>
|
||||
<v-list-item
|
||||
v-for="nav in baseLinks"
|
||||
:key="nav.title"
|
||||
link
|
||||
:to="nav.to"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ nav.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ nav.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-list nav dense>
|
||||
<v-list-item
|
||||
v-for="nav in superLinks"
|
||||
:key="nav.title"
|
||||
link
|
||||
:to="nav.to"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ nav.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ nav.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showSidebar: false,
|
||||
mobile: false,
|
||||
links: [],
|
||||
superLinks: [
|
||||
{
|
||||
icon: "mdi-cog",
|
||||
to: "/admin/settings",
|
||||
title: "Site Settings",
|
||||
},
|
||||
{
|
||||
icon: "mdi-account-group",
|
||||
to: "/admin/manage-users",
|
||||
title: "Manage Users",
|
||||
},
|
||||
{
|
||||
icon: "mdi-backup-restore",
|
||||
to: "/admin/backups",
|
||||
title: "Backups",
|
||||
},
|
||||
{
|
||||
icon: "mdi-database-import",
|
||||
to: "/admin/migrations",
|
||||
title: "Migrations",
|
||||
},
|
||||
],
|
||||
baseLinks: [
|
||||
{
|
||||
icon: "mdi-account",
|
||||
to: "/admin/profile",
|
||||
title: "Profile",
|
||||
},
|
||||
{
|
||||
icon: "mdi-format-color-fill",
|
||||
to: "/admin/themes",
|
||||
title: "Themes",
|
||||
},
|
||||
{
|
||||
icon: "mdi-food",
|
||||
to: "/admin/meal-planner",
|
||||
title: "Meal Planner",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.mobile = this.viewScale();
|
||||
this.showSidebar = !this.viewScale();
|
||||
},
|
||||
|
||||
methods: {
|
||||
viewScale() {
|
||||
switch (this.$vuetify.breakpoint.name) {
|
||||
case "xs":
|
||||
return true;
|
||||
case "sm":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -2,10 +2,18 @@
|
|||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="70%">
|
||||
<v-card>
|
||||
<v-card-title> Import Summary </v-card-title>
|
||||
<v-card-text>
|
||||
<v-row class="mb-n9">
|
||||
<v-card flat>
|
||||
<v-app-bar dark color="primary mb-2">
|
||||
<v-icon large left>
|
||||
mdi-import
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headline">
|
||||
Import Summary
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-card-text class="mb-n4">
|
||||
<v-row>
|
||||
<div>
|
||||
<v-card-text>
|
||||
<div>
|
||||
<h3>Recipes</h3>
|
||||
|
@ -17,8 +25,8 @@
|
|||
Failed: {{ recipeNumbers.failure }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card flat>
|
||||
</div>
|
||||
<div>
|
||||
<v-card-text>
|
||||
<div>
|
||||
<h3>Themes</h3>
|
||||
|
@ -30,8 +38,8 @@
|
|||
Failed: {{ themeNumbers.failure }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card flat>
|
||||
</div>
|
||||
<div>
|
||||
<v-card-text>
|
||||
<div>
|
||||
<h3>Settings</h3>
|
||||
|
@ -43,7 +51,7 @@
|
|||
Failed: {{ settingsNumbers.failure }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-tabs v-model="tab">
|
|
@ -25,17 +25,19 @@
|
|||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-card outlined min-height="250">
|
||||
<v-card-text class="pt-2 pb-1">
|
||||
<h3>{{$t('settings.homepage.homepage-categories')}}</h3>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-list
|
||||
min-height="200"
|
||||
dense
|
||||
max-height="200"
|
||||
style="overflow:auto"
|
||||
>
|
||||
<v-card outlined min-height="350px">
|
||||
<v-app-bar dark dense color="primary">
|
||||
<v-icon left>
|
||||
mdi-home
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
Home Page Categories
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-list height="300" dense style="overflow:auto">
|
||||
<v-list-item-group>
|
||||
<draggable
|
||||
v-model="homeCategories"
|
||||
|
@ -65,24 +67,19 @@
|
|||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-card outlined min-height="250px">
|
||||
<v-card-text class="pt-2 pb-1">
|
||||
<h3>
|
||||
{{$t('settings.homepage.all-categories')}}
|
||||
<span>
|
||||
<v-btn absolute right x-small color="success" icon>
|
||||
<v-icon>mdi-plus</v-icon></v-btn
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-list
|
||||
min-height="200"
|
||||
dense
|
||||
max-height="200"
|
||||
style="overflow:auto"
|
||||
>
|
||||
<v-card outlined height="350px">
|
||||
<v-app-bar dark dense color="primary">
|
||||
<v-icon left>
|
||||
mdi-tag
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
All Categories
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-list height="300" dense style="overflow:auto">
|
||||
<v-list-item-group>
|
||||
<draggable
|
||||
v-model="categories"
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-card class="my-2" :loading="loading">
|
||||
<v-card outlined class="my-2" :loading="loading">
|
||||
<v-card-title>
|
||||
{{ title }}
|
||||
<v-spacer></v-spacer>
|
29
frontend/src/components/Login/LoginDialog.vue
Normal file
29
frontend/src/components/Login/LoginDialog.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="500">
|
||||
<LoginForm @logged-in="dialog = false" />
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoginForm from "./LoginForm";
|
||||
export default {
|
||||
components: {
|
||||
LoginForm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.dialog = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
145
frontend/src/components/Login/LoginForm.vue
Normal file
145
frontend/src/components/Login/LoginForm.vue
Normal file
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-card max-width="500px">
|
||||
<v-divider></v-divider>
|
||||
<v-app-bar dark color="primary mt-n1">
|
||||
<v-icon large left v-if="!loading">
|
||||
mdi-account
|
||||
</v-icon>
|
||||
<v-progress-circular
|
||||
v-else
|
||||
indeterminate
|
||||
color="white"
|
||||
large
|
||||
class="mr-2"
|
||||
>
|
||||
</v-progress-circular>
|
||||
<v-toolbar-title class="headline"> Login </v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
v-if="!options.isLoggingIn"
|
||||
v-model="user.name"
|
||||
light="light"
|
||||
prepend-icon="person"
|
||||
:label="$t('general.name')"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.email"
|
||||
light="light"
|
||||
prepend-icon="mdi-email"
|
||||
:label="$t('login.email')"
|
||||
type="email"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.password"
|
||||
light="light"
|
||||
prepend-icon="mdi-lock"
|
||||
:label="$t('login.password')"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
<v-checkbox
|
||||
class="mb-2 mt-0"
|
||||
v-if="options.isLoggingIn"
|
||||
v-model="options.shouldStayLoggedIn"
|
||||
light="light"
|
||||
:label="$t('login.stay-logged-in')"
|
||||
hide-details="hide-details"
|
||||
></v-checkbox>
|
||||
<v-btn
|
||||
v-if="options.isLoggingIn"
|
||||
@click.prevent="login"
|
||||
dark
|
||||
color="primary"
|
||||
block="block"
|
||||
type="submit"
|
||||
>{{ $t("login.sign-in") }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
v-else
|
||||
block="block"
|
||||
type="submit"
|
||||
@click.prevent="options.isLoggingIn = true"
|
||||
>{{ $t("login.sign-up") }}</v-btn
|
||||
>
|
||||
</v-form>
|
||||
<v-alert v-if="error" outlined class="mt-3 mb-0" type="error">
|
||||
Could Not Validate Credentials
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<!-- <v-card-actions v-if="options.isLoggingIn" class="card-actions">
|
||||
<div>
|
||||
Don't have an account?
|
||||
</div>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
light="light"
|
||||
@click="options.isLoggingIn = false"
|
||||
>
|
||||
Sign up
|
||||
</v-btn>
|
||||
</v-card-actions> -->
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
export default {
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
error: false,
|
||||
showLogin: false,
|
||||
showPassword: false,
|
||||
user: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
options: {
|
||||
isLoggingIn: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.clear();
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
this.user = { email: "", password: "" };
|
||||
},
|
||||
async login() {
|
||||
this.loading = true;
|
||||
this.error = false;
|
||||
let formData = new FormData();
|
||||
formData.append("username", this.user.email);
|
||||
formData.append("password", this.user.password);
|
||||
let key;
|
||||
try {
|
||||
key = await api.users.login(formData);
|
||||
} catch {
|
||||
this.error = true;
|
||||
}
|
||||
if (key.status != 200) this.error = true;
|
||||
else {
|
||||
this.$emit("logged-in");
|
||||
this.clear();
|
||||
}
|
||||
console.log(key);
|
||||
this.$store.commit("setToken", key.data.access_token)
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
<script>
|
||||
import utils from "@/utils";
|
||||
import SearchDialog from "../UI/SearchDialog";
|
||||
import SearchDialog from "../UI/Search/SearchDialog";
|
||||
export default {
|
||||
components: {
|
||||
SearchDialog,
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Confirmation from "./Confirmation";
|
||||
import Confirmation from "../../components/UI/Confirmation";
|
||||
|
||||
export default {
|
||||
props: {
|
|
@ -2,9 +2,25 @@
|
|||
<div class="text-center">
|
||||
<v-dialog v-model="addRecipe" width="650" @click:outside="reset">
|
||||
<v-card :loading="processing">
|
||||
<v-card-title class="headline"
|
||||
>{{ $t("new-recipe.from-url") }}
|
||||
</v-card-title>
|
||||
<v-app-bar dark color="primary mb-2">
|
||||
<v-icon large left v-if="!processing">
|
||||
mdi-link
|
||||
</v-icon>
|
||||
<v-progress-circular
|
||||
v-else
|
||||
indeterminate
|
||||
color="white"
|
||||
large
|
||||
class="mr-2"
|
||||
>
|
||||
</v-progress-circular>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ $t("new-recipe.from-url") }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="urlForm">
|
||||
|
@ -14,7 +30,10 @@
|
|||
required
|
||||
validate-on-blur
|
||||
autofocus
|
||||
class="mt-1"
|
||||
:rules="[isValidWebUrl]"
|
||||
:hint="$t('new-recipe.url-form-hint')"
|
||||
persistent-hint
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
|
||||
|
@ -30,7 +49,12 @@
|
|||
<v-btn color="grey" text @click="reset">
|
||||
{{ $t("general.close") }}
|
||||
</v-btn>
|
||||
<v-btn color="success" text @click="createRecipe">
|
||||
<v-btn
|
||||
color="success"
|
||||
text
|
||||
@click="createRecipe"
|
||||
:loading="processing"
|
||||
>
|
||||
{{ $t("general.submit") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import RecipeCard from "./RecipeCard";
|
||||
import RecipeCard from "../Recipe/RecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
RecipeCard,
|
||||
|
|
|
@ -21,14 +21,20 @@
|
|||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="cancel"> {{ $t("general.cancel") }} </v-btn>
|
||||
<v-btn :color="color" text @click="confirm"> {{ $t("general.confirm") }} </v-btn>
|
||||
<v-btn color="grey" text @click="cancel">
|
||||
{{ $t("general.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn :color="color" text @click="confirm">
|
||||
{{ $t("general.confirm") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const CLOSE_EVENT = "close";
|
||||
const OPEN_EVENT = "open";
|
||||
/**
|
||||
* Confirmation Component used to add a second validaion step to an action.
|
||||
* @version 1.0.1
|
||||
|
@ -51,7 +57,7 @@ export default {
|
|||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: "mid-alert-circle"
|
||||
default: "mid-alert-circle",
|
||||
},
|
||||
/**
|
||||
* Color theme of the component. Chose one of the defined theme colors.
|
||||
|
@ -59,28 +65,35 @@ export default {
|
|||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "error"
|
||||
default: "error",
|
||||
},
|
||||
/**
|
||||
* Define the max width of the component.
|
||||
*/
|
||||
width: {
|
||||
type: Number,
|
||||
default: 400
|
||||
default: 400,
|
||||
},
|
||||
/**
|
||||
* zIndex of the component.
|
||||
*/
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 200
|
||||
}
|
||||
default: 200,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
dialog() {
|
||||
if (this.dialog === false) {
|
||||
this.$emit(CLOSE_EVENT);
|
||||
} else this.$emit(OPEN_EVENT);
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
/**
|
||||
* Keep state of open or closed
|
||||
*/
|
||||
dialog: false
|
||||
dialog: false,
|
||||
}),
|
||||
methods: {
|
||||
/**
|
||||
|
@ -120,8 +133,8 @@ export default {
|
|||
|
||||
//Hide Modal
|
||||
this.dialog = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
80
frontend/src/components/UI/LanguageMenu.vue
Normal file
80
frontend/src/components/UI/LanguageMenu.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<LoginDialog ref="loginDialog" />
|
||||
<v-menu
|
||||
transition="slide-x-transition"
|
||||
bottom
|
||||
right
|
||||
offset-y
|
||||
close-delay="200"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" icon>
|
||||
<v-icon>mdi-translate</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item-group v-model="selectedItem" color="primary">
|
||||
<v-list-item
|
||||
v-for="(item, i) in allLanguages"
|
||||
:key="i"
|
||||
link
|
||||
@click="setLanguage(item.value)"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ item.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoginDialog from "../Login/LoginDialog";
|
||||
export default {
|
||||
components: {
|
||||
LoginDialog,
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
selectedItem: 0,
|
||||
items: [
|
||||
{
|
||||
name: "English",
|
||||
value: "en",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
let active = this.$store.getters.getActiveLang;
|
||||
this.allLanguages.forEach((element, index) => {
|
||||
if (element.value === active) {
|
||||
this.selectedItem = index;
|
||||
return;
|
||||
}
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
allLanguages() {
|
||||
return this.$store.getters.getAllLangs;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
setLanguage(selectedLanguage) {
|
||||
this.$store.commit("setLang", selectedLanguage);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.menu-text {
|
||||
text-align: left !important;
|
||||
}
|
||||
</style>
|
|
@ -1,99 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-btn icon @click="showLogin = true">
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</v-btn>
|
||||
<v-dialog v-model="showLogin" width="500">
|
||||
<v-flex class="login-form text-xs-center">
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
v-if="!options.isLoggingIn"
|
||||
v-model="user.name"
|
||||
light="light"
|
||||
prepend-icon="person"
|
||||
:label="$t('general.name')"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.email"
|
||||
light="light"
|
||||
prepend-icon="mdi-email"
|
||||
:label="$t('login.email')"
|
||||
type="email"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.password"
|
||||
light="light"
|
||||
prepend-icon="mdi-lock"
|
||||
:label="$t('login.password')"
|
||||
type="password"
|
||||
></v-text-field>
|
||||
<v-checkbox
|
||||
class="mb-2 mt-0"
|
||||
v-if="options.isLoggingIn"
|
||||
v-model="options.shouldStayLoggedIn"
|
||||
light="light"
|
||||
:label="$t('login.stay-logged-in')"
|
||||
hide-details="hide-details"
|
||||
></v-checkbox>
|
||||
<v-btn
|
||||
v-if="options.isLoggingIn"
|
||||
@click.prevent="login"
|
||||
dark
|
||||
color="primary"
|
||||
block="block"
|
||||
type="submit"
|
||||
>{{ $t("login.sign-in") }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
v-else
|
||||
block="block"
|
||||
type="submit"
|
||||
@click.prevent="options.isLoggingIn = true"
|
||||
>{{ $t("login.sign-up") }}</v-btn
|
||||
>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<!-- <v-card-actions v-if="options.isLoggingIn" class="card-actions">
|
||||
Don't have an account?
|
||||
<v-btn
|
||||
color="primary"
|
||||
light="light"
|
||||
@click="options.isLoggingIn = false"
|
||||
>
|
||||
Sign up
|
||||
</v-btn>
|
||||
</v-card-actions> -->
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
export default {
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
showLogin: false,
|
||||
user: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
options: {
|
||||
isLoggingIn: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
let key = await api.login(this.user.email, this.user.password);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -43,8 +43,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import SearchBar from "../UI/SearchBar";
|
||||
import RecipeCard from "../UI/RecipeCard";
|
||||
import SearchBar from "./SearchBar";
|
||||
import RecipeCard from "../../Recipe/RecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
SearchBar,
|
|
@ -1,27 +0,0 @@
|
|||
<template>
|
||||
<v-row>
|
||||
<v-col cols="2"> </v-col>
|
||||
<v-col>
|
||||
<v-expand-transition>
|
||||
<Search class="search-bar" />
|
||||
</v-expand-transition>
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-btn icon>
|
||||
<v-icon> mdi-filter </v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Search from "./Search";
|
||||
export default {
|
||||
components: {
|
||||
Search,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<LoginDialog ref="loginDialog" />
|
||||
<v-menu
|
||||
transition="slide-x-transition"
|
||||
bottom
|
||||
|
@ -15,7 +16,13 @@
|
|||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item v-for="(item, i) in items" :key="i" link :to="item.nav">
|
||||
<v-list-item
|
||||
v-for="(item, i) in filteredItems"
|
||||
:key="i"
|
||||
link
|
||||
:to="item.nav ? item.nav : null"
|
||||
@click="item.login ? openLoginDialog() : null"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
@ -31,36 +38,70 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import LoginDialog from "../Login/LoginDialog";
|
||||
export default {
|
||||
components: {
|
||||
LoginDialog,
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
icon: "mdi-account",
|
||||
title: "Login",
|
||||
restricted: false,
|
||||
login: true,
|
||||
},
|
||||
{
|
||||
icon: "mdi-calendar-week",
|
||||
title: this.$i18n.t("meal-plan.dinner-this-week"),
|
||||
nav: "/meal-plan/this-week",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: "mdi-calendar-today",
|
||||
title: this.$i18n.t("meal-plan.dinner-today"),
|
||||
nav: "/meal-plan/today",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: "mdi-calendar-multiselect",
|
||||
title: this.$i18n.t("meal-plan.planner"),
|
||||
nav: "/meal-plan/planner",
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: "mdi-account",
|
||||
title: "Logout",
|
||||
restricted: true,
|
||||
nav: "/logout",
|
||||
},
|
||||
{
|
||||
icon: "mdi-cog",
|
||||
title: this.$i18n.t("general.settings"),
|
||||
nav: "/settings/site",
|
||||
nav: "/admin",
|
||||
restricted: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
mounted() {},
|
||||
computed: {
|
||||
loggedIn() {
|
||||
return this.$store.getters.getIsLoggedIn;
|
||||
},
|
||||
filteredItems() {
|
||||
if (this.loggedIn) {
|
||||
return this.items.filter(x => x.restricted == true);
|
||||
} else {
|
||||
return this.items.filter(x => x.restricted == false);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
navRouter(route) {
|
||||
this.$router.push(route);
|
||||
openLoginDialog() {
|
||||
this.$refs.loginDialog.open();
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,41 +0,0 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-snackbar :value="active" :timeout="timeout" :color="type">
|
||||
{{ text }}
|
||||
|
||||
<template v-slot:action="{ attrs }">
|
||||
<v-btn color="white" text v-bind="attrs" @click="close(false)">
|
||||
{{$t('general.close')}}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
snackbar: false,
|
||||
timeout: -1,
|
||||
}),
|
||||
computed: {
|
||||
text() {
|
||||
return this.$store.getters.getSnackText;
|
||||
},
|
||||
active() {
|
||||
return this.$store.getters.getSnackActive;
|
||||
},
|
||||
type() {
|
||||
return this.$store.getters.getSnackType;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close(value) {
|
||||
this.$store.commit("setSnackActive", value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -12,8 +12,9 @@
|
|||
"take-me-home": "Take me Home"
|
||||
},
|
||||
"new-recipe": {
|
||||
"from-url": "From URL",
|
||||
"from-url": "Import a Recipe",
|
||||
"recipe-url": "Recipe URL",
|
||||
"url-form-hint": "Copy and paste a link from your favorite recipe website",
|
||||
"error-message": "Looks like there was an error parsing the URL. Check the log and debug/last_recipe.json to see what went wrong.",
|
||||
"bulk-add": "Bulk Add",
|
||||
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Paste in your recipe data. Each line will be treated as an item in a list"
|
||||
|
@ -97,6 +98,8 @@
|
|||
},
|
||||
"settings": {
|
||||
"general-settings": "General Settings",
|
||||
"change-password": "Change Password",
|
||||
"admin-settings": "Admin Settings",
|
||||
"local-api": "Local API",
|
||||
"language": "Language",
|
||||
"add-a-new-theme": "Add a New Theme",
|
||||
|
|
|
@ -48,11 +48,11 @@
|
|||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||
import ImportSummaryDialog from "./ImportSummaryDialog";
|
||||
import UploadBtn from "../../UI/UploadBtn";
|
||||
import AvailableBackupCard from "./AvailableBackupCard";
|
||||
import NewBackupCard from "./NewBackupCard";
|
||||
import SuccessFailureAlert from "@/components/UI/SuccessFailureAlert";
|
||||
import ImportSummaryDialog from "@/components/Admin/Backup/ImportSummaryDialog";
|
||||
import UploadBtn from "@/components/UI/UploadBtn";
|
||||
import AvailableBackupCard from "@/components/Admin/Backup/AvailableBackupCard";
|
||||
import NewBackupCard from "@/components/Admin/Backup/NewBackupCard";
|
||||
|
||||
export default {
|
||||
components: {
|
243
frontend/src/pages/Admin/ManageUsers/index.vue
Normal file
243
frontend/src/pages/Admin/ManageUsers/index.vue
Normal file
|
@ -0,0 +1,243 @@
|
|||
<template>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="users"
|
||||
sort-by="calories"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template v-slot:top>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>Mealie Users</v-toolbar-title>
|
||||
<v-divider class="mx-4" inset vertical></v-divider>
|
||||
<v-spacer></v-spacer>
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn color="primary" dark class="mb-2" v-bind="attrs" v-on="on">
|
||||
Create User
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-app-bar dark dense color="primary">
|
||||
<v-icon left>
|
||||
mdi-account
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ formTitle }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-title class="headline">
|
||||
User ID: {{ editedItem.id }}
|
||||
</v-toolbar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.fullName"
|
||||
label="Full Name"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.email"
|
||||
label="Email"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.family"
|
||||
label="Family Group"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6" v-if="showPassword">
|
||||
<v-text-field
|
||||
v-model="editedItem.password"
|
||||
label="User Password"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-switch
|
||||
v-model="editedItem.admin"
|
||||
label="Admin"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="close">
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="save">
|
||||
Save
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<Confirmation
|
||||
ref="deleteUserDialog"
|
||||
title="Confirm User Deletion"
|
||||
:message="
|
||||
`Are you sure you want to delete the user <b>${activeName} ID: ${activeId}<b/>`
|
||||
"
|
||||
icon="mdi-alert"
|
||||
@confirm="deleteUser"
|
||||
:width="450"
|
||||
@close="closeDelete"
|
||||
/>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn class="mr-1" small color="error" @click="deleteItem(item)">
|
||||
<v-icon small left>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
Delete
|
||||
</v-btn>
|
||||
<v-btn small color="success" @click="editItem(item)">
|
||||
<v-icon small left class="mr-2">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
Edit
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:item.admin="{ item }">
|
||||
{{ item.admin ? "Admin" : "User" }}
|
||||
</template>
|
||||
<template v-slot:no-data>
|
||||
<v-btn color="primary" @click="initialize">
|
||||
Reset
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
import api from "@/api";
|
||||
export default {
|
||||
components: { Confirmation },
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
activeId: null,
|
||||
activeName: null,
|
||||
headers: [
|
||||
{
|
||||
text: "User ID",
|
||||
align: "start",
|
||||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ text: "Full Name", value: "fullName" },
|
||||
{ text: "Email", value: "email" },
|
||||
{ text: "Family", value: "family" },
|
||||
{ text: "Admin", value: "admin" },
|
||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
||||
],
|
||||
users: [],
|
||||
editedIndex: -1,
|
||||
editedItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
},
|
||||
defaultItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
},
|
||||
}),
|
||||
|
||||
computed: {
|
||||
formTitle() {
|
||||
return this.editedIndex === -1 ? "New User" : "Edit User";
|
||||
},
|
||||
showPassword() {
|
||||
return this.editedIndex === -1 ? true : false;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close();
|
||||
},
|
||||
dialogDelete(val) {
|
||||
val || this.closeDelete();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.users = await api.users.allUsers();
|
||||
},
|
||||
|
||||
async deleteUser() {
|
||||
await api.users.delete(this.editedIndex);
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
editItem(item) {
|
||||
this.editedIndex = this.users.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.dialog = true;
|
||||
},
|
||||
|
||||
deleteItem(item) {
|
||||
this.activeId = item.id;
|
||||
this.activeName = item.fullName;
|
||||
this.editedIndex = this.users.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.$refs.deleteUserDialog.open();
|
||||
},
|
||||
|
||||
deleteItemConfirm() {
|
||||
this.users.splice(this.editedIndex, 1);
|
||||
this.closeDelete();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
closeDelete() {
|
||||
this.dialogDelete = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
console.log("New User", this.editedItem);
|
||||
api.users.update(this.editedItem);
|
||||
} else {
|
||||
api.users.create(this.editedItem);
|
||||
}
|
||||
await this.initialize();
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -97,7 +97,7 @@
|
|||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import TimePickerDialog from "./TimePickerDialog";
|
||||
import TimePickerDialog from "@/components/Admin/MealPlanner/TimePickerDialog";
|
||||
export default {
|
||||
components: {
|
||||
TimePickerDialog,
|
|
@ -13,8 +13,8 @@
|
|||
{{ $t("migration.recipe-migration") }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
</v-card>
|
||||
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
<v-col
|
||||
:cols="12"
|
||||
|
@ -35,13 +35,15 @@
|
|||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import MigrationCard from "./MigrationCard";
|
||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
||||
import MigrationCard from "@/components/Admin/Migration/MigrationCard";
|
||||
import SuccessFailureAlert from "@/components/UI/SuccessFailureAlert";
|
||||
import api from "@/api";
|
||||
export default {
|
||||
components: {
|
92
frontend/src/pages/Admin/Profile/index.vue
Normal file
92
frontend/src/pages/Admin/Profile/index.vue
Normal file
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<span>
|
||||
<v-avatar color="accent" size="40" class="mr-2" v-if="!loading">
|
||||
<img src="https://cdn.vuetifyjs.com/images/john.jpg" alt="John" />
|
||||
</v-avatar>
|
||||
<v-progress-circular
|
||||
v-else
|
||||
indeterminate
|
||||
color="primary"
|
||||
large
|
||||
class="mr-2"
|
||||
>
|
||||
</v-progress-circular>
|
||||
</span>
|
||||
Profile
|
||||
<v-spacer></v-spacer>
|
||||
User ID: {{ user.id }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field label="Full Name" v-model="user.fullName"> </v-text-field>
|
||||
<v-text-field label="Email" v-model="user.email"> </v-text-field>
|
||||
<v-text-field
|
||||
label="Family"
|
||||
readonly
|
||||
v-model="user.family"
|
||||
persistent-hint
|
||||
hint="Family groups can only be set by administrators"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="accent" class="mr-2">
|
||||
<v-icon left> mdi-lock </v-icon>
|
||||
{{ $t("settings.change-password") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" class="mr-2" @click="updateUser">
|
||||
<v-icon left> mdi-content-save </v-icon>
|
||||
{{ $t("general.save") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import AvatarPicker from '@/components/AvatarPicker'
|
||||
import api from "@/api";
|
||||
export default {
|
||||
pageTitle: "My Profile",
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
user: {
|
||||
fullName: "Change Me",
|
||||
email: "changeme@email.com",
|
||||
family: "public",
|
||||
admin: true,
|
||||
id: 1,
|
||||
},
|
||||
showAvatarPicker: false,
|
||||
};
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.refreshProfile();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async refreshProfile() {
|
||||
this.user = await api.users.self();
|
||||
},
|
||||
openAvatarPicker() {
|
||||
this.showAvatarPicker = true;
|
||||
},
|
||||
selectAvatar(avatar) {
|
||||
this.user.avatar = avatar;
|
||||
},
|
||||
async updateUser() {
|
||||
this.loading = true;
|
||||
let newKey = await api.users.update(this.user);
|
||||
this.$store.commit("setToken", newKey.access_token);
|
||||
this.refreshProfile();
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ $t("settings.general-settings") }}
|
||||
<v-card-title class="headline">
|
||||
{{ $t("settings.admin-settings") }}
|
||||
<v-spacer></v-spacer>
|
||||
<span>
|
||||
<v-btn class="pt-1" text href="/docs">
|
||||
|
@ -10,23 +10,7 @@
|
|||
</v-btn>
|
||||
</span>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<h2 class="mt-1 mb-4">{{ $t("settings.language") }}</h2>
|
||||
<v-row>
|
||||
<v-col sm="3">
|
||||
<v-select
|
||||
dense
|
||||
v-model="selectedLang"
|
||||
:items="langOptions"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
:label="$t('settings.language')"
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<HomePageSettings />
|
||||
<v-divider></v-divider>
|
||||
|
@ -34,7 +18,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import HomePageSettings from "./HomePageSettings";
|
||||
import HomePageSettings from "@/components/Admin/General/HomePageSettings";
|
||||
|
||||
export default {
|
||||
components: {
|
|
@ -150,9 +150,9 @@
|
|||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
import ColorPickerDialog from "./ColorPickerDialog";
|
||||
import NewThemeDialog from "./NewThemeDialog";
|
||||
import Confirmation from "../../UI/Confirmation";
|
||||
import ColorPickerDialog from "@/components/Admin/Theme/ColorPickerDialog";
|
||||
import NewThemeDialog from "@/components/Admin/Theme/NewThemeDialog";
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
|
||||
export default {
|
||||
components: {
|
20
frontend/src/pages/Admin/index.vue
Normal file
20
frontend/src/pages/Admin/index.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div>
|
||||
<AdminSidebar />
|
||||
<v-slide-x-transition hide-on-leave>
|
||||
<router-view></router-view>
|
||||
</v-slide-x-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AdminSidebar from "../../components/Admin/AdminSidebar";
|
||||
export default {
|
||||
components: {
|
||||
AdminSidebar,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
19
frontend/src/pages/LoginPage.vue
Normal file
19
frontend/src/pages/LoginPage.vue
Normal file
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<v-row justify="start" height="100%">
|
||||
<v-col align="center">
|
||||
<LoginForm />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoginForm from "../components/Login/LoginForm";
|
||||
export default {
|
||||
components: {
|
||||
LoginForm,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -12,7 +12,7 @@
|
|||
</v-img>
|
||||
<br v-else />
|
||||
|
||||
<ButtonRow
|
||||
<EditorButtonRow
|
||||
@json="jsonEditor = true"
|
||||
@editor="jsonEditor = false"
|
||||
@save="createRecipe"
|
||||
|
@ -43,12 +43,12 @@ import api from "@/api";
|
|||
|
||||
import RecipeEditor from "../components/Recipe/RecipeEditor";
|
||||
import VJsoneditor from "v-jsoneditor";
|
||||
import ButtonRow from "../components/UI/ButtonRow";
|
||||
import EditorButtonRow from "../components/Recipe/EditorButtonRow";
|
||||
export default {
|
||||
components: {
|
||||
VJsoneditor,
|
||||
RecipeEditor,
|
||||
ButtonRow,
|
||||
EditorButtonRow,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
:performTime="recipeDetails.performTime"
|
||||
/>
|
||||
</v-img>
|
||||
<ButtonRow
|
||||
<EditorButtonRow
|
||||
:open="showIcons"
|
||||
@json="jsonEditor = true"
|
||||
@editor="
|
||||
|
@ -62,14 +62,14 @@ import VJsoneditor from "v-jsoneditor";
|
|||
import RecipeViewer from "../components/Recipe/RecipeViewer";
|
||||
import RecipeEditor from "../components/Recipe/RecipeEditor";
|
||||
import RecipeTimeCard from "../components/Recipe/RecipeTimeCard.vue";
|
||||
import ButtonRow from "../components/UI/ButtonRow";
|
||||
import EditorButtonRow from "../components/Recipe/EditorButtonRow";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VJsoneditor,
|
||||
RecipeViewer,
|
||||
RecipeEditor,
|
||||
ButtonRow,
|
||||
EditorButtonRow,
|
||||
RecipeTimeCard,
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -34,8 +34,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import SearchBar from "../components/UI/SearchBar";
|
||||
import RecipeCard from "../components/UI/RecipeCard";
|
||||
import SearchBar from "../components/UI/Search/SearchBar";
|
||||
import RecipeCard from "../components/Recipe/RecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
SearchBar,
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<v-alert
|
||||
v-if="newVersion"
|
||||
color="green"
|
||||
type="success"
|
||||
outlined
|
||||
v-html="
|
||||
$t('settings.new-version-available', {
|
||||
aContents:
|
||||
'target=\'_blank\' href=\'https://github.com/hay-kot/mealie\' class=\'green--text\'',
|
||||
})
|
||||
"
|
||||
>
|
||||
</v-alert>
|
||||
<General />
|
||||
<Theme class="mt-2" />
|
||||
<Backup class="mt-2" />
|
||||
<MealPlanner class="mt-2" />
|
||||
<Migration class="mt-2" />
|
||||
<p class="text-center my-2">
|
||||
{{ $t("settings.current") }}
|
||||
{{ version }} |
|
||||
{{ $t("settings.latest") }}
|
||||
{{ latestVersion }}
|
||||
·
|
||||
<a href="https://hay-kot.github.io/mealie/" target="_blank">
|
||||
{{ $t("settings.explore-the-docs") }}
|
||||
</a>
|
||||
·
|
||||
<a
|
||||
href="https://hay-kot.github.io/mealie/contributors/non-coders/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t("settings.contribute") }}
|
||||
</a>
|
||||
</p>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Backup from "../components/Settings/Backup";
|
||||
import General from "../components/Settings/General";
|
||||
import MealPlanner from "../components/Settings/MealPlanner";
|
||||
import Theme from "../components/Settings/Theme";
|
||||
import Migration from "../components/Settings/Migration";
|
||||
import api from "@/api";
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Backup,
|
||||
MealPlanner,
|
||||
Theme,
|
||||
Migration,
|
||||
General,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
latestVersion: null,
|
||||
version: null,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.getVersion();
|
||||
let versionData = await api.meta.get_version();
|
||||
this.version = versionData.version;
|
||||
},
|
||||
computed: {
|
||||
newVersion() {
|
||||
if ((this.latestVersion != null) & (this.latestVersion != this.version)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getVersion() {
|
||||
let response = await axios.get(
|
||||
"https://api.github.com/repos/hay-kot/mealie/releases/latest"
|
||||
);
|
||||
this.latestVersion = response.data.tag_name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,40 +0,0 @@
|
|||
import HomePage from "./pages/HomePage";
|
||||
import Page404 from "./pages/404Page";
|
||||
import SearchPage from "./pages/SearchPage";
|
||||
import RecipePage from "./pages/RecipePage";
|
||||
import RecipeNewPage from "./pages/RecipeNewPage";
|
||||
import SettingsPage from "./pages/SettingsPage";
|
||||
import AllRecipesPage from "./pages/AllRecipesPage";
|
||||
import CategoryPage from "./pages/CategoryPage";
|
||||
import MeaplPlanPage from "./pages/MealPlanPage";
|
||||
import Debug from "./pages/Debug";
|
||||
import MealPlanThisWeekPage from "./pages/MealPlanThisWeekPage";
|
||||
import api from "@/api";
|
||||
|
||||
export const routes = [
|
||||
{ path: "/", component: HomePage },
|
||||
{ path: "/mealie", component: HomePage },
|
||||
{ path: "/debug", component: Debug },
|
||||
{ path: "/search", component: SearchPage },
|
||||
{ path: "/recipes/all", component: AllRecipesPage },
|
||||
{ path: "/recipes/:category", component: CategoryPage },
|
||||
{ path: "/recipe/:recipe", component: RecipePage },
|
||||
{ path: "/new/", component: RecipeNewPage },
|
||||
{ path: "/settings/site", component: SettingsPage },
|
||||
{ path: "/meal-plan/planner", component: MeaplPlanPage },
|
||||
{ path: "/meal-plan/this-week", component: MealPlanThisWeekPage },
|
||||
{
|
||||
path: "/meal-plan/today",
|
||||
beforeEnter: async (_to, _from, next) => {
|
||||
await todaysMealRoute().then(redirect => {
|
||||
next(redirect);
|
||||
});
|
||||
},
|
||||
},
|
||||
{ path: "*", component: Page404 },
|
||||
];
|
||||
|
||||
async function todaysMealRoute() {
|
||||
const response = await api.mealPlans.today();
|
||||
return "/recipe/" + response.data;
|
||||
}
|
48
frontend/src/routes/admin.js
Normal file
48
frontend/src/routes/admin.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
import Admin from "@/pages/Admin";
|
||||
import Backup from "@/pages/Admin/Backup";
|
||||
import Theme from "@/pages/Admin/Theme";
|
||||
import MealPlanner from "@/pages/Admin/MealPlanner";
|
||||
import Migration from "@/pages/Admin/Migration";
|
||||
import Profile from "@/pages/Admin/Profile";
|
||||
import ManageUsers from "@/pages/Admin/ManageUsers";
|
||||
import Settings from "@/pages/Admin/Settings";
|
||||
|
||||
export default {
|
||||
path: "/admin",
|
||||
component: Admin,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: Profile,
|
||||
},
|
||||
{
|
||||
path: "profile",
|
||||
component: Profile,
|
||||
},
|
||||
|
||||
{
|
||||
path: "backups",
|
||||
component: Backup,
|
||||
},
|
||||
{
|
||||
path: "themes",
|
||||
component: Theme,
|
||||
},
|
||||
{
|
||||
path: "meal-planner",
|
||||
component: MealPlanner,
|
||||
},
|
||||
{
|
||||
path: "migrations",
|
||||
component: Migration,
|
||||
},
|
||||
{
|
||||
path: "manage-users",
|
||||
component: ManageUsers,
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
component: Settings,
|
||||
},
|
||||
],
|
||||
};
|
51
frontend/src/routes/index.js
Normal file
51
frontend/src/routes/index.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import HomePage from "../pages/HomePage";
|
||||
import Page404 from "../pages/404Page";
|
||||
import SearchPage from "../pages/SearchPage";
|
||||
import RecipePage from "../pages/RecipePage";
|
||||
import RecipeNewPage from "../pages/RecipeNewPage";
|
||||
import AllRecipesPage from "../pages/AllRecipesPage";
|
||||
import CategoryPage from "../pages/CategoryPage";
|
||||
import MeaplPlanPage from "../pages/MealPlanPage";
|
||||
import Debug from "../pages/Debug";
|
||||
import LoginPage from "../pages/LoginPage";
|
||||
import MealPlanThisWeekPage from "../pages/MealPlanThisWeekPage";
|
||||
import api from "@/api";
|
||||
import Admin from "./admin";
|
||||
import { store } from "../store/store";
|
||||
|
||||
export const routes = [
|
||||
{ path: "/", name: "home", component: HomePage },
|
||||
{
|
||||
path: "/logout",
|
||||
beforeEnter: (_to, _from, next) => {
|
||||
store.commit("setToken", "");
|
||||
store.commit("setIsLoggedIn", false);
|
||||
next("/");
|
||||
},
|
||||
},
|
||||
{ path: "/mealie", component: HomePage },
|
||||
{ path: "/login", component: LoginPage },
|
||||
{ path: "/debug", component: Debug },
|
||||
{ path: "/search", component: SearchPage },
|
||||
{ path: "/recipes/all", component: AllRecipesPage },
|
||||
{ path: "/recipes/:category", component: CategoryPage },
|
||||
{ path: "/recipe/:recipe", component: RecipePage },
|
||||
{ path: "/new/", component: RecipeNewPage },
|
||||
{ path: "/meal-plan/planner", component: MeaplPlanPage },
|
||||
{ path: "/meal-plan/this-week", component: MealPlanThisWeekPage },
|
||||
Admin,
|
||||
{
|
||||
path: "/meal-plan/today",
|
||||
beforeEnter: async (_to, _from, next) => {
|
||||
await todaysMealRoute().then(redirect => {
|
||||
next(redirect);
|
||||
});
|
||||
},
|
||||
},
|
||||
{ path: "*", component: Page404 },
|
||||
];
|
||||
|
||||
async function todaysMealRoute() {
|
||||
const response = await api.mealPlans.today();
|
||||
return "/recipe/" + response.data;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import api from "@/api";
|
||||
import Vuetify from "../../plugins/vuetify";
|
||||
import axios from "axios";
|
||||
|
||||
function inDarkMode(payload) {
|
||||
let isDark;
|
||||
|
@ -18,6 +19,8 @@ const state = {
|
|||
activeTheme: {},
|
||||
darkMode: "system",
|
||||
isDark: false,
|
||||
isLoggedIn: false,
|
||||
token: "",
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
|
@ -35,6 +38,14 @@ const mutations = {
|
|||
state.darkMode = payload;
|
||||
}
|
||||
},
|
||||
setIsLoggedIn(state, payload) {
|
||||
state.isLoggedIn = payload;
|
||||
},
|
||||
setToken(state, payload) {
|
||||
state.isLoggedIn = true;
|
||||
axios.defaults.headers.common["Authorization"] = `Bearer ${payload}`;
|
||||
state.token = payload;
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
|
@ -47,6 +58,7 @@ const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
|
||||
async initTheme({ dispatch, getters }) {
|
||||
//If theme is empty resetTheme
|
||||
if (Object.keys(getters.getActiveTheme).length === 0) {
|
||||
|
@ -63,6 +75,8 @@ const getters = {
|
|||
getActiveTheme: state => state.activeTheme,
|
||||
getDarkMode: state => state.darkMode,
|
||||
getIsDark: state => state.isDark,
|
||||
getIsLoggedIn: state => state.isLoggedIn,
|
||||
getToken: state => state.token,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -20,11 +20,6 @@ const store = new Vuex.Store({
|
|||
homePage,
|
||||
},
|
||||
state: {
|
||||
// Home Page Settings
|
||||
// Snackbar
|
||||
snackActive: false,
|
||||
snackText: "",
|
||||
snackType: "warning",
|
||||
|
||||
// All Recipe Data Store
|
||||
recentRecipes: [],
|
||||
|
@ -33,15 +28,6 @@ const store = new Vuex.Store({
|
|||
},
|
||||
|
||||
mutations: {
|
||||
setSnackBar(state, payload) {
|
||||
state.snackText = payload.text;
|
||||
state.snackType = payload.type;
|
||||
state.snackActive = true;
|
||||
},
|
||||
setSnackActive(state, payload) {
|
||||
state.snackActive = payload;
|
||||
},
|
||||
|
||||
setRecentRecipes(state, payload) {
|
||||
state.recentRecipes = payload;
|
||||
},
|
||||
|
@ -68,11 +54,6 @@ const store = new Vuex.Store({
|
|||
},
|
||||
|
||||
getters: {
|
||||
//
|
||||
getSnackText: state => state.snackText,
|
||||
getSnackActive: state => state.snackActive,
|
||||
getSnackType: state => state.snackType,
|
||||
|
||||
getRecentRecipes: state => state.recentRecipes,
|
||||
getMealPlanCategories: state => state.mealPlanCategories,
|
||||
},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const path = require("path");
|
||||
module.exports = {
|
||||
transpileDependencies: ["vuetify"],
|
||||
publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
|
||||
|
@ -18,4 +19,11 @@ module.exports = {
|
|||
enableInSFC: true,
|
||||
},
|
||||
},
|
||||
configureWebpack: {
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve("src"),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -2,7 +2,9 @@ import uvicorn
|
|||
from fastapi import FastAPI
|
||||
|
||||
# import utils.startup as startup
|
||||
from app_config import APP_VERSION, PORT, PRODUCTION, docs_url, redoc_url
|
||||
from core.config import APP_VERSION, PORT, SECRET, docs_url, redoc_url
|
||||
from db.db_setup import sql_exists
|
||||
from db.init_db import init_db
|
||||
from routes import (
|
||||
backup_routes,
|
||||
debug_routes,
|
||||
|
@ -17,8 +19,8 @@ from routes.recipe import (
|
|||
recipe_crud_routes,
|
||||
tag_routes,
|
||||
)
|
||||
from services.settings_services import default_settings_init
|
||||
from utils.logger import logger
|
||||
from routes.users import users
|
||||
from fastapi.logger import logger
|
||||
|
||||
app = FastAPI(
|
||||
title="Mealie",
|
||||
|
@ -29,16 +31,17 @@ app = FastAPI(
|
|||
)
|
||||
|
||||
|
||||
def data_base_first_run():
|
||||
init_db()
|
||||
|
||||
|
||||
def start_scheduler():
|
||||
import services.scheduler.scheduled_jobs
|
||||
|
||||
|
||||
def init_settings():
|
||||
default_settings_init()
|
||||
import services.theme_services
|
||||
|
||||
|
||||
def api_routers():
|
||||
# Authentication
|
||||
app.include_router(users.router)
|
||||
# Recipes
|
||||
app.include_router(all_recipe_routes.router)
|
||||
app.include_router(category_routes.router)
|
||||
|
@ -56,10 +59,11 @@ def api_routers():
|
|||
app.include_router(debug_routes.router)
|
||||
|
||||
|
||||
if not sql_exists:
|
||||
data_base_first_run()
|
||||
|
||||
api_routers()
|
||||
start_scheduler()
|
||||
init_settings()
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("-----SYSTEM STARTUP-----")
|
||||
|
|
|
@ -15,9 +15,12 @@ def ensure_dirs():
|
|||
ENV = CWD.joinpath(".env")
|
||||
dotenv.load_dotenv(ENV)
|
||||
|
||||
|
||||
SECRET = "super-secret-key"
|
||||
|
||||
# General
|
||||
APP_VERSION = "v0.3.0"
|
||||
DB_VERSION = "v0.2.1"
|
||||
DB_VERSION = "v0.3.0"
|
||||
PRODUCTION = os.environ.get("ENV")
|
||||
PORT = int(os.getenv("mealie_port", 9000))
|
||||
API = os.getenv("api_docs", True)
|
||||
|
@ -30,7 +33,7 @@ else:
|
|||
redoc_url = None
|
||||
|
||||
# Helpful Globals
|
||||
DATA_DIR = CWD.parent.joinpath("app_data")
|
||||
DATA_DIR = CWD.parent.parent.joinpath("app_data")
|
||||
if PRODUCTION:
|
||||
DATA_DIR = Path("/app/data")
|
||||
|
||||
|
@ -59,6 +62,8 @@ REQUIRED_DIRS = [
|
|||
|
||||
ensure_dirs()
|
||||
|
||||
LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
|
||||
|
||||
|
||||
# DATABASE ENV
|
||||
SQLITE_FILE = None
|
29
mealie/core/security.py
Normal file
29
mealie/core/security.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
from passlib.context import CryptContext
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""Compares a plain string to a hashed password
|
||||
|
||||
Args:
|
||||
plain_password (str): raw password string
|
||||
hashed_password (str): hashed password from the database
|
||||
|
||||
Returns:
|
||||
bool: Returns True if a match return False
|
||||
"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""Takes in a raw password and hashes it. Used prior to saving
|
||||
a new password to the database.
|
||||
|
||||
Args:
|
||||
password (str): Password String
|
||||
|
||||
Returns:
|
||||
str: Hashed Password
|
||||
"""
|
||||
return pwd_context.hash(password)
|
|
@ -1,10 +1,11 @@
|
|||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from db.db_base import BaseDocument
|
||||
from db.sql.meal_models import MealPlanModel
|
||||
from db.sql.recipe_models import Category, RecipeModel, Tag
|
||||
from db.sql.settings_models import SiteSettingsModel
|
||||
from db.sql.theme_models import SiteThemeModel
|
||||
from db.models.mealplan import MealPlanModel
|
||||
from db.models.recipe import Category, RecipeModel, Tag
|
||||
from db.models.settings import SiteSettingsModel
|
||||
from db.models.theme import SiteThemeModel
|
||||
from db.models.users import User
|
||||
|
||||
"""
|
||||
# TODO
|
||||
|
@ -55,6 +56,12 @@ class _Themes(BaseDocument):
|
|||
self.sql_model = SiteThemeModel
|
||||
|
||||
|
||||
class _Users(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "id"
|
||||
self.sql_model = User
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self) -> None:
|
||||
self.recipes = _Recipes()
|
||||
|
@ -63,6 +70,7 @@ class Database:
|
|||
self.themes = _Themes()
|
||||
self.categories = _Categories()
|
||||
self.tags = _Tags()
|
||||
self.users = _Users()
|
||||
|
||||
|
||||
db = Database()
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import List
|
|||
from sqlalchemy.orm import load_only
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from db.sql.model_base import SqlAlchemyBase
|
||||
from db.models.model_base import SqlAlchemyBase
|
||||
|
||||
|
||||
class BaseDocument:
|
||||
|
@ -108,7 +108,10 @@ class BaseDocument:
|
|||
db_entries = [x.dict() for x in result]
|
||||
|
||||
if limit == 1:
|
||||
try:
|
||||
return db_entries[0]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
return db_entries
|
||||
|
||||
|
@ -124,9 +127,8 @@ class BaseDocument:
|
|||
"""
|
||||
new_document = self.sql_model(session=session, **document)
|
||||
session.add(new_document)
|
||||
return_data = new_document.dict()
|
||||
session.commit()
|
||||
|
||||
return_data = new_document.dict()
|
||||
return return_data
|
||||
|
||||
def update(self, session: Session, match_value: str, new_data: str) -> dict:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from app_config import SQLITE_FILE, USE_SQL
|
||||
from core.config import SQLITE_FILE, USE_SQL
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from db.sql.db_session import sql_global_init
|
||||
from db.models.db_session import sql_global_init
|
||||
|
||||
sql_exists = True
|
||||
|
||||
|
|
63
mealie/db/init_db.py
Normal file
63
mealie/db/init_db.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
from core.security import get_password_hash
|
||||
from fastapi.logger import logger
|
||||
from schema.settings import SiteSettings, Webhooks
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from db.database import db
|
||||
from db.db_setup import create_session
|
||||
|
||||
|
||||
def init_db(db: Session = None) -> None:
|
||||
if not db:
|
||||
db = create_session()
|
||||
|
||||
default_settings_init(db)
|
||||
default_theme_init(db)
|
||||
default_user_init(db)
|
||||
|
||||
db.close()
|
||||
|
||||
|
||||
def default_theme_init(session: Session):
|
||||
default_theme = {
|
||||
"name": "default",
|
||||
"colors": {
|
||||
"primary": "#E58325",
|
||||
"accent": "#00457A",
|
||||
"secondary": "#973542",
|
||||
"success": "#5AB1BB",
|
||||
"info": "#4990BA",
|
||||
"warning": "#FF4081",
|
||||
"error": "#EF5350",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
db.themes.create(session, default_theme)
|
||||
logger.info("Generating default theme...")
|
||||
except:
|
||||
logger.info("Default Theme Exists.. skipping generation")
|
||||
|
||||
|
||||
def default_settings_init(session: Session):
|
||||
try:
|
||||
webhooks = Webhooks()
|
||||
default_entry = SiteSettings(name="main", webhooks=webhooks)
|
||||
document = db.settings.create(session, default_entry.dict())
|
||||
logger.info(f"Created Site Settings: \n {document}")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def default_user_init(session: Session):
|
||||
default_user = {
|
||||
"full_name": "Change Me",
|
||||
"email": "changeme@email.com",
|
||||
"password": get_password_hash("MyPassword"),
|
||||
"family": "public",
|
||||
"admin": True,
|
||||
}
|
||||
|
||||
logger.info("Generating Default User")
|
||||
db.users.create(session, default_user)
|
5
mealie/db/models/_all_models.py
Normal file
5
mealie/db/models/_all_models.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from db.models.mealplan import *
|
||||
from db.models.recipe import *
|
||||
from db.models.settings import *
|
||||
from db.models.theme import *
|
||||
from db.models.users import *
|
|
@ -1,7 +1,7 @@
|
|||
from pathlib import Path
|
||||
|
||||
import sqlalchemy as sa
|
||||
from db.sql.model_base import SqlAlchemyBase
|
||||
from db.models.model_base import SqlAlchemyBase
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
|
||||
|
@ -18,7 +18,7 @@ def sql_global_init(db_file: Path, check_thread=False):
|
|||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
import db.sql._all_models
|
||||
import db.models._all_models
|
||||
|
||||
SqlAlchemyBase.metadata.create_all(engine)
|
||||
|
|
@ -3,7 +3,7 @@ from typing import List
|
|||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
||||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
|
||||
|
||||
class Meal(SqlAlchemyBase):
|
|
@ -4,11 +4,11 @@ from typing import List
|
|||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
||||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from slugify import slugify
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
from sqlalchemy.orm import validates
|
||||
from utils.logger import logger
|
||||
from fastapi.logger import logger
|
||||
|
||||
|
||||
class ApiExtras(SqlAlchemyBase):
|
|
@ -1,7 +1,6 @@
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
||||
from db.sql.recipe_models import Category
|
||||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
|
||||
|
||||
class SiteSettingsModel(SqlAlchemyBase, BaseMixins):
|
|
@ -1,6 +1,6 @@
|
|||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm as orm
|
||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
||||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
|
||||
|
||||
class SiteThemeModel(SqlAlchemyBase):
|
44
mealie/db/models/users.py
Normal file
44
mealie/db/models/users.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from sqlalchemy import Boolean, Column, Integer, String
|
||||
|
||||
|
||||
class User(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "users"
|
||||
id = Column(Integer, primary_key=True)
|
||||
full_name = Column(String, index=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
password = Column(String)
|
||||
is_active = Column(Boolean(), default=True)
|
||||
family = Column(String)
|
||||
admin = Column(Boolean(), default=False)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session,
|
||||
full_name,
|
||||
email,
|
||||
password,
|
||||
family="public",
|
||||
admin=False,
|
||||
) -> None:
|
||||
self.full_name = full_name
|
||||
self.email = email
|
||||
self.family = family
|
||||
self.admin = admin
|
||||
self.password = password
|
||||
|
||||
def dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"full_name": self.full_name,
|
||||
"email": self.email,
|
||||
"admin": self.admin,
|
||||
"family": self.family,
|
||||
"password": self.password,
|
||||
}
|
||||
|
||||
def update(self, full_name, email, family, admin, session=None):
|
||||
self.full_name = full_name
|
||||
self.email = email
|
||||
self.family = family
|
||||
self.admin = admin
|
|
@ -1,4 +0,0 @@
|
|||
from db.sql.meal_models import *
|
||||
from db.sql.recipe_models import *
|
||||
from db.sql.settings_models import *
|
||||
from db.sql.theme_models import *
|
0
mealie/routes/__init__.py
Normal file
0
mealie/routes/__init__.py
Normal file
|
@ -1,15 +1,15 @@
|
|||
import operator
|
||||
import shutil
|
||||
|
||||
from app_config import BACKUP_DIR, TEMPLATE_DIR
|
||||
from core.config import BACKUP_DIR, TEMPLATE_DIR
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from models.backup_models import BackupJob, ImportJob, Imports, LocalBackup
|
||||
from schema.backup import BackupJob, ImportJob, Imports, LocalBackup
|
||||
from services.backups.exports import backup_all
|
||||
from services.backups.imports import ImportDatabase
|
||||
from sqlalchemy.orm.session import Session
|
||||
from starlette.responses import FileResponse
|
||||
from utils.snackbar import SnackResponse
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(prefix="/api/backups", tags=["Backups"])
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import json
|
||||
|
||||
from app_config import APP_VERSION, DEBUG_DIR
|
||||
from core.config import APP_VERSION, DEBUG_DIR, LOGGER_FILE
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import HTMLResponse
|
||||
from utils.logger import LOGGER_FILE
|
||||
|
||||
router = APIRouter(prefix="/api/debug", tags=["Debug"])
|
||||
|
||||
|
|
23
mealie/routes/deps.py
Normal file
23
mealie/routes/deps.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from core.config import SECRET
|
||||
from db.database import db
|
||||
from db.db_setup import create_session
|
||||
from fastapi_login import LoginManager
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from schema.user import UserInDB
|
||||
|
||||
manager = LoginManager(SECRET, "/api/auth/token")
|
||||
|
||||
|
||||
@manager.user_loader
|
||||
def query_user(user_email: str, session: Session = None) -> UserInDB:
|
||||
"""
|
||||
Get a user from the db
|
||||
:param user_id: E-Mail of the user
|
||||
:return: None or the UserInDB object
|
||||
"""
|
||||
|
||||
session = session if session else create_session()
|
||||
user = db.users.get(session, user_email, "email")
|
||||
session.close()
|
||||
return UserInDB(**user)
|
|
@ -5,7 +5,7 @@ from db.db_setup import generate_session
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from services.meal_services import MealPlan
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.snackbar import SnackResponse
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@ import operator
|
|||
import shutil
|
||||
from typing import List
|
||||
|
||||
from app_config import MIGRATION_DIR
|
||||
from core.config import MIGRATION_DIR
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from models.migration_models import MigrationFile, Migrations
|
||||
from schema.migration import MigrationFile, Migrations
|
||||
from services.migrations.chowdown import chowdown_migrate as chowdow_migrate
|
||||
from services.migrations.nextcloud import migrate as nextcloud_migrate
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.snackbar import SnackResponse
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(prefix="/api/migrations", tags=["Migration"])
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import List, Optional
|
|||
from db.database import db
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from models.recipe_models import AllRecipeRequest
|
||||
from schema.recipe import AllRecipeRequest
|
||||
from slugify import slugify
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
from db.database import db
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from models.category_models import RecipeCategoryResponse
|
||||
from schema.category import RecipeCategoryResponse
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.snackbar import SnackResponse
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
from utils.snackbar import SnackResponse
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/categories",
|
||||
|
@ -27,7 +27,6 @@ def get_all_recipes_by_category(
|
|||
return db.categories.get(session, category)
|
||||
|
||||
|
||||
|
||||
@router.delete("/{category}")
|
||||
async def delete_recipe_category(
|
||||
category: str, session: Session = Depends(generate_session)
|
||||
|
|
|
@ -2,12 +2,12 @@ from db.db_setup import generate_session
|
|||
from fastapi import APIRouter, Depends, File, Form, HTTPException
|
||||
from fastapi.logger import logger
|
||||
from fastapi.responses import FileResponse
|
||||
from models.recipe_models import RecipeURLIn
|
||||
from schema.recipe import RecipeURLIn
|
||||
from services.image_services import read_image, write_image
|
||||
from services.recipe_services import Recipe
|
||||
from services.scraper.scraper import create_from_url
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.snackbar import SnackResponse
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/recipes",
|
||||
|
|
|
@ -2,9 +2,9 @@ from db.database import db
|
|||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.snackbar import SnackResponse
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
from utils.snackbar import SnackResponse
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(tags=["Recipes"])
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
from db.database import db
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from models.settings_models import SiteSettings
|
||||
from services.settings_services import default_settings_init
|
||||
from schema.settings import SiteSettings
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.post_webhooks import post_webhooks
|
||||
from utils.snackbar import SnackResponse
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
|
||||
|
||||
|
@ -17,8 +16,7 @@ def get_main_settings(session: Session = Depends(generate_session)):
|
|||
try:
|
||||
data = db.settings.get(session, "main")
|
||||
except:
|
||||
default_settings_init(session)
|
||||
data = db.settings.get(session, "main")
|
||||
return
|
||||
return data
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from models.theme_models import SiteTheme
|
||||
from schema.theme import SiteTheme
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.snackbar import SnackResponse
|
||||
from schema.snackbar import SnackResponse
|
||||
from db.database import db
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["Themes"])
|
||||
|
|
0
mealie/routes/users/__init__.py
Normal file
0
mealie/routes/users/__init__.py
Normal file
32
mealie/routes/users/auth.py
Normal file
32
mealie/routes/users/auth.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from core.security import verify_password
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from fastapi_login.exceptions import InvalidCredentialsException
|
||||
from routes.deps import manager, query_user
|
||||
from schema.user import UserInDB
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["Auth"])
|
||||
|
||||
|
||||
@router.post("/token")
|
||||
def token(
|
||||
data: OAuth2PasswordRequestForm = Depends(),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
email = data.username
|
||||
password = data.password
|
||||
|
||||
user: UserInDB = query_user(email, session)
|
||||
if not user:
|
||||
raise InvalidCredentialsException # you can also use your own HTTPException
|
||||
elif not verify_password(password, user.password):
|
||||
raise InvalidCredentialsException
|
||||
|
||||
access_token = manager.create_access_token(
|
||||
data=dict(sub=email), expires=timedelta(hours=2)
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
85
mealie/routes/users/crud.py
Normal file
85
mealie/routes/users/crud.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from core.security import get_password_hash
|
||||
from db.database import db
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from routes.deps import manager, query_user
|
||||
from schema.user import UserBase, UserIn, UserInDB, UserOut
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["Users"])
|
||||
|
||||
|
||||
@router.post("", response_model=UserOut, status_code=201)
|
||||
async def create_user(
|
||||
new_user: UserIn,
|
||||
current_user=Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns a list of all user in the Database """
|
||||
|
||||
new_user.password = get_password_hash(new_user.password)
|
||||
|
||||
data = db.users.create(session, new_user.dict())
|
||||
return data
|
||||
|
||||
|
||||
@router.get("", response_model=list[UserOut])
|
||||
async def get_all_users(
|
||||
current_user: UserInDB = Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
||||
if current_user.admin:
|
||||
return db.users.get_all(session)
|
||||
else:
|
||||
return {"details": "user not authorized"}
|
||||
|
||||
|
||||
@router.get("/self", response_model=UserOut)
|
||||
async def get_user_by_id(
|
||||
current_user: UserInDB = Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
return current_user.dict()
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=UserOut)
|
||||
async def get_user_by_id(
|
||||
id: int,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
return db.users.get(session, id)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_user(
|
||||
id: int,
|
||||
new_data: UserBase,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
||||
if current_user.id == id or current_user.admin:
|
||||
updated_user = db.users.update(session, id, new_data.dict())
|
||||
email = updated_user.get("email")
|
||||
if current_user.id == id:
|
||||
access_token = manager.create_access_token(
|
||||
data=dict(sub=email), expires=timedelta(hours=2)
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
return
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_user(
|
||||
id: int,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Removes a user from the database. Must be the current user or a super user"""
|
||||
|
||||
if current_user.id == id or current_user.admin:
|
||||
return db.users.delete(session, id)
|
7
mealie/routes/users/users.py
Normal file
7
mealie/routes/users/users.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from fastapi import APIRouter
|
||||
from routes.users import auth, crud
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(auth.router)
|
||||
router.include_router(crud.router)
|
32
mealie/schema/user.py
Normal file
32
mealie/schema/user.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from typing import Optional
|
||||
|
||||
from fastapi_camelcase import CamelModel
|
||||
|
||||
# from pydantic import EmailStr
|
||||
|
||||
|
||||
class UserBase(CamelModel):
|
||||
full_name: Optional[str] = None
|
||||
email: str
|
||||
family: str
|
||||
admin: bool
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"fullName": "Change Me",
|
||||
"email": "changeme@email.com",
|
||||
"family": "public",
|
||||
"admin": "false",
|
||||
}
|
||||
|
||||
|
||||
class UserIn(UserBase):
|
||||
password: str
|
||||
|
||||
|
||||
class UserOut(UserBase):
|
||||
id: int
|
||||
|
||||
|
||||
class UserInDB(UserIn, UserOut):
|
||||
pass
|
|
@ -3,13 +3,13 @@ import shutil
|
|||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
|
||||
from core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
|
||||
from db.database import db
|
||||
from db.db_setup import create_session
|
||||
from jinja2 import Template
|
||||
from services.meal_services import MealPlan
|
||||
from services.recipe_services import Recipe
|
||||
from utils.logger import logger
|
||||
from fastapi.logger import logger
|
||||
|
||||
|
||||
class ExportDatabase:
|
||||
|
|
|
@ -4,13 +4,13 @@ import zipfile
|
|||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from app_config import BACKUP_DIR, IMG_DIR, TEMP_DIR
|
||||
from core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR
|
||||
from db.database import db
|
||||
from models.import_models import RecipeImport, SettingsImport, ThemeImport
|
||||
from models.theme_models import SiteTheme
|
||||
from schema.restore import RecipeImport, SettingsImport, ThemeImport
|
||||
from schema.theme import SiteTheme
|
||||
from services.recipe_services import Recipe
|
||||
from sqlalchemy.orm.session import Session
|
||||
from utils.logger import logger
|
||||
from fastapi.logger import logger
|
||||
|
||||
|
||||
class ImportDatabase:
|
||||
|
|
|
@ -2,8 +2,8 @@ import shutil
|
|||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from app_config import IMG_DIR
|
||||
from utils.logger import logger
|
||||
from core.config import IMG_DIR
|
||||
from fastapi.logger import logger
|
||||
|
||||
|
||||
def read_image(recipe_slug: str) -> Path:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue