mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
Feature/authentication (#185)
* basic crud NOT SECURE * refactor/database init on startup * added scratch.py * tests/user CRUD routes * password hashing * change app_config location * bump python version * formatting * login ui starter * change import from url design * move components * remove old snackbar * refactor/Componenet folder structure rework * refactor/remove old code * refactor/rename componenets/js files * remove console.logs * refactor/ models to schema and sql to models * new header styling for imports * token request * fix url scrapper * refactor/rename schema files * split routes file * redesigned admin page * enable relative imports for vue components * refactor/switch to pages view * add CamelCase package * majors settings rework * user management second pass * super user CRUD * refactor/consistent models names * refactor/consistent model names Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
6d6e0574b2
commit
f5fa4040bf
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
|
mealie/data/debug/last_recipe.json
|
||||||
*.sqlite
|
*.sqlite
|
||||||
app_data/db/test.db
|
app_data/db/test.db
|
||||||
|
scratch.py
|
||||||
|
|
|
@ -27,12 +27,12 @@
|
||||||
<v-icon>mdi-magnify</v-icon>
|
<v-icon>mdi-magnify</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<Menu />
|
<SiteMenu />
|
||||||
|
<LanguageMenu />
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<v-main>
|
<v-main>
|
||||||
<v-container>
|
<v-container>
|
||||||
<AddRecipeFab />
|
<AddRecipeFab />
|
||||||
<SnackBar />
|
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</v-container>
|
</v-container>
|
||||||
<FlashMessage :position="'right bottom'"></FlashMessage>
|
<FlashMessage :position="'right bottom'"></FlashMessage>
|
||||||
|
@ -41,19 +41,20 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Menu from "./components/UI/Menu";
|
import SiteMenu from "@/components/UI/SiteMenu";
|
||||||
import SearchBar from "./components/UI/SearchBar";
|
import SearchBar from "@/components/UI/Search/SearchBar";
|
||||||
import AddRecipeFab from "./components/UI/AddRecipeFab";
|
import AddRecipeFab from "@/components/UI/AddRecipeFab";
|
||||||
import SnackBar from "./components/UI/SnackBar";
|
import LanguageMenu from "@/components/UI/LanguageMenu";
|
||||||
import Vuetify from "./plugins/vuetify";
|
import Vuetify from "./plugins/vuetify";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Menu,
|
SiteMenu,
|
||||||
AddRecipeFab,
|
AddRecipeFab,
|
||||||
SnackBar,
|
|
||||||
SearchBar,
|
SearchBar,
|
||||||
|
LanguageMenu,
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -63,7 +64,7 @@ export default {
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
window.addEventListener("keyup", e => {
|
window.addEventListener("keyup", e => {
|
||||||
if (e.key == "/" && !document.activeElement.id.startsWith('input') ) {
|
if (e.key == "/" && !document.activeElement.id.startsWith("input")) {
|
||||||
this.search = !this.search;
|
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/";
|
const baseURL = "/api/";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import utils from "@/utils";
|
import utils from "@/utils";
|
||||||
|
import { store } from "../store/store";
|
||||||
|
|
||||||
|
axios.defaults.headers.common[
|
||||||
|
"Authorization"
|
||||||
|
] = `Bearer ${store.getters.getToken}`;
|
||||||
|
|
||||||
function processResponse(response) {
|
function processResponse(response) {
|
||||||
try {
|
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(`/`);
|
router.push(`/`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async allByKeys(recipeKeys, num = 100) {
|
async allByKeys(recipeKeys, num = 999) {
|
||||||
const response = await apiReq.get(recipeURLs.allRecipes, {
|
const response = await apiReq.get(recipeURLs.allRecipes, {
|
||||||
params: {
|
params: {
|
||||||
keys: recipeKeys,
|
keys: recipeKeys,
|
||||||
|
|
|
@ -33,7 +33,6 @@ export default {
|
||||||
colors: colors,
|
colors: colors,
|
||||||
};
|
};
|
||||||
let response = await apiReq.put(settingsURLs.updateTheme(themeName), body);
|
let response = await apiReq.put(settingsURLs.updateTheme(themeName), body);
|
||||||
console.log(response.data);
|
|
||||||
return 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">
|
<div class="text-center">
|
||||||
<v-dialog v-model="dialog" width="70%">
|
<v-dialog v-model="dialog" width="70%">
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title> Import Summary </v-card-title>
|
<v-app-bar dark color="primary mb-2">
|
||||||
<v-card-text>
|
<v-icon large left>
|
||||||
<v-row class="mb-n9">
|
mdi-import
|
||||||
<v-card flat>
|
</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>
|
<v-card-text>
|
||||||
<div>
|
<div>
|
||||||
<h3>Recipes</h3>
|
<h3>Recipes</h3>
|
||||||
|
@ -17,8 +25,8 @@
|
||||||
Failed: {{ recipeNumbers.failure }}
|
Failed: {{ recipeNumbers.failure }}
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</div>
|
||||||
<v-card flat>
|
<div>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div>
|
<div>
|
||||||
<h3>Themes</h3>
|
<h3>Themes</h3>
|
||||||
|
@ -30,8 +38,8 @@
|
||||||
Failed: {{ themeNumbers.failure }}
|
Failed: {{ themeNumbers.failure }}
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</div>
|
||||||
<v-card flat>
|
<div>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div>
|
<div>
|
||||||
<h3>Settings</h3>
|
<h3>Settings</h3>
|
||||||
|
@ -43,7 +51,7 @@
|
||||||
Failed: {{ settingsNumbers.failure }}
|
Failed: {{ settingsNumbers.failure }}
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</div>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-tabs v-model="tab">
|
<v-tabs v-model="tab">
|
|
@ -25,17 +25,19 @@
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" sm="6">
|
<v-col cols="12" sm="6">
|
||||||
<v-card outlined min-height="250">
|
<v-card outlined min-height="350px">
|
||||||
<v-card-text class="pt-2 pb-1">
|
<v-app-bar dark dense color="primary">
|
||||||
<h3>{{$t('settings.homepage.homepage-categories')}}</h3>
|
<v-icon left>
|
||||||
</v-card-text>
|
mdi-home
|
||||||
<v-divider></v-divider>
|
</v-icon>
|
||||||
<v-list
|
|
||||||
min-height="200"
|
<v-toolbar-title class="headline">
|
||||||
dense
|
Home Page Categories
|
||||||
max-height="200"
|
</v-toolbar-title>
|
||||||
style="overflow:auto"
|
|
||||||
>
|
<v-spacer></v-spacer>
|
||||||
|
</v-app-bar>
|
||||||
|
<v-list height="300" dense style="overflow:auto">
|
||||||
<v-list-item-group>
|
<v-list-item-group>
|
||||||
<draggable
|
<draggable
|
||||||
v-model="homeCategories"
|
v-model="homeCategories"
|
||||||
|
@ -65,24 +67,19 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="6">
|
<v-col cols="12" sm="6">
|
||||||
<v-card outlined min-height="250px">
|
<v-card outlined height="350px">
|
||||||
<v-card-text class="pt-2 pb-1">
|
<v-app-bar dark dense color="primary">
|
||||||
<h3>
|
<v-icon left>
|
||||||
{{$t('settings.homepage.all-categories')}}
|
mdi-tag
|
||||||
<span>
|
</v-icon>
|
||||||
<v-btn absolute right x-small color="success" icon>
|
|
||||||
<v-icon>mdi-plus</v-icon></v-btn
|
<v-toolbar-title class="headline">
|
||||||
>
|
All Categories
|
||||||
</span>
|
</v-toolbar-title>
|
||||||
</h3>
|
|
||||||
</v-card-text>
|
<v-spacer></v-spacer>
|
||||||
<v-divider></v-divider>
|
</v-app-bar>
|
||||||
<v-list
|
<v-list height="300" dense style="overflow:auto">
|
||||||
min-height="200"
|
|
||||||
dense
|
|
||||||
max-height="200"
|
|
||||||
style="overflow:auto"
|
|
||||||
>
|
|
||||||
<v-list-item-group>
|
<v-list-item-group>
|
||||||
<draggable
|
<draggable
|
||||||
v-model="categories"
|
v-model="categories"
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<v-card class="my-2" :loading="loading">
|
<v-card outlined class="my-2" :loading="loading">
|
||||||
<v-card-title>
|
<v-card-title>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
<v-spacer></v-spacer>
|
<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>
|
<script>
|
||||||
import utils from "@/utils";
|
import utils from "@/utils";
|
||||||
import SearchDialog from "../UI/SearchDialog";
|
import SearchDialog from "../UI/Search/SearchDialog";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SearchDialog,
|
SearchDialog,
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Confirmation from "./Confirmation";
|
import Confirmation from "../../components/UI/Confirmation";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
|
@ -2,9 +2,25 @@
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<v-dialog v-model="addRecipe" width="650" @click:outside="reset">
|
<v-dialog v-model="addRecipe" width="650" @click:outside="reset">
|
||||||
<v-card :loading="processing">
|
<v-card :loading="processing">
|
||||||
<v-card-title class="headline"
|
<v-app-bar dark color="primary mb-2">
|
||||||
>{{ $t("new-recipe.from-url") }}
|
<v-icon large left v-if="!processing">
|
||||||
</v-card-title>
|
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-card-text>
|
||||||
<v-form ref="urlForm">
|
<v-form ref="urlForm">
|
||||||
|
@ -14,7 +30,10 @@
|
||||||
required
|
required
|
||||||
validate-on-blur
|
validate-on-blur
|
||||||
autofocus
|
autofocus
|
||||||
|
class="mt-1"
|
||||||
:rules="[isValidWebUrl]"
|
:rules="[isValidWebUrl]"
|
||||||
|
:hint="$t('new-recipe.url-form-hint')"
|
||||||
|
persistent-hint
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</v-form>
|
</v-form>
|
||||||
|
|
||||||
|
@ -30,7 +49,12 @@
|
||||||
<v-btn color="grey" text @click="reset">
|
<v-btn color="grey" text @click="reset">
|
||||||
{{ $t("general.close") }}
|
{{ $t("general.close") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn color="success" text @click="createRecipe">
|
<v-btn
|
||||||
|
color="success"
|
||||||
|
text
|
||||||
|
@click="createRecipe"
|
||||||
|
:loading="processing"
|
||||||
|
>
|
||||||
{{ $t("general.submit") }}
|
{{ $t("general.submit") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import RecipeCard from "./RecipeCard";
|
import RecipeCard from "../Recipe/RecipeCard";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
RecipeCard,
|
RecipeCard,
|
||||||
|
|
|
@ -21,14 +21,20 @@
|
||||||
|
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn color="grey" text @click="cancel"> {{ $t("general.cancel") }} </v-btn>
|
<v-btn color="grey" text @click="cancel">
|
||||||
<v-btn :color="color" text @click="confirm"> {{ $t("general.confirm") }} </v-btn>
|
{{ $t("general.cancel") }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn :color="color" text @click="confirm">
|
||||||
|
{{ $t("general.confirm") }}
|
||||||
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const CLOSE_EVENT = "close";
|
||||||
|
const OPEN_EVENT = "open";
|
||||||
/**
|
/**
|
||||||
* Confirmation Component used to add a second validaion step to an action.
|
* Confirmation Component used to add a second validaion step to an action.
|
||||||
* @version 1.0.1
|
* @version 1.0.1
|
||||||
|
@ -51,7 +57,7 @@ export default {
|
||||||
*/
|
*/
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "mid-alert-circle"
|
default: "mid-alert-circle",
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Color theme of the component. Chose one of the defined theme colors.
|
* Color theme of the component. Chose one of the defined theme colors.
|
||||||
|
@ -59,28 +65,35 @@ export default {
|
||||||
*/
|
*/
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "error"
|
default: "error",
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Define the max width of the component.
|
* Define the max width of the component.
|
||||||
*/
|
*/
|
||||||
width: {
|
width: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 400
|
default: 400,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* zIndex of the component.
|
* zIndex of the component.
|
||||||
*/
|
*/
|
||||||
zIndex: {
|
zIndex: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 200
|
default: 200,
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
dialog() {
|
||||||
|
if (this.dialog === false) {
|
||||||
|
this.$emit(CLOSE_EVENT);
|
||||||
|
} else this.$emit(OPEN_EVENT);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
/**
|
/**
|
||||||
* Keep state of open or closed
|
* Keep state of open or closed
|
||||||
*/
|
*/
|
||||||
dialog: false
|
dialog: false,
|
||||||
}),
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
/**
|
/**
|
||||||
|
@ -120,8 +133,8 @@ export default {
|
||||||
|
|
||||||
//Hide Modal
|
//Hide Modal
|
||||||
this.dialog = false;
|
this.dialog = false;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SearchBar from "../UI/SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import RecipeCard from "../UI/RecipeCard";
|
import RecipeCard from "../../Recipe/RecipeCard";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SearchBar,
|
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>
|
<template>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
<LoginDialog ref="loginDialog" />
|
||||||
<v-menu
|
<v-menu
|
||||||
transition="slide-x-transition"
|
transition="slide-x-transition"
|
||||||
bottom
|
bottom
|
||||||
|
@ -15,7 +16,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-list>
|
<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-list-item-icon>
|
||||||
<v-icon>{{ item.icon }}</v-icon>
|
<v-icon>{{ item.icon }}</v-icon>
|
||||||
</v-list-item-icon>
|
</v-list-item-icon>
|
||||||
|
@ -31,36 +38,70 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import LoginDialog from "../Login/LoginDialog";
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
LoginDialog,
|
||||||
|
},
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
icon: "mdi-account",
|
||||||
|
title: "Login",
|
||||||
|
restricted: false,
|
||||||
|
login: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: "mdi-calendar-week",
|
icon: "mdi-calendar-week",
|
||||||
title: this.$i18n.t("meal-plan.dinner-this-week"),
|
title: this.$i18n.t("meal-plan.dinner-this-week"),
|
||||||
nav: "/meal-plan/this-week",
|
nav: "/meal-plan/this-week",
|
||||||
|
restricted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "mdi-calendar-today",
|
icon: "mdi-calendar-today",
|
||||||
title: this.$i18n.t("meal-plan.dinner-today"),
|
title: this.$i18n.t("meal-plan.dinner-today"),
|
||||||
nav: "/meal-plan/today",
|
nav: "/meal-plan/today",
|
||||||
|
restricted: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "mdi-calendar-multiselect",
|
icon: "mdi-calendar-multiselect",
|
||||||
title: this.$i18n.t("meal-plan.planner"),
|
title: this.$i18n.t("meal-plan.planner"),
|
||||||
nav: "/meal-plan/planner",
|
nav: "/meal-plan/planner",
|
||||||
|
restricted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "mdi-account",
|
||||||
|
title: "Logout",
|
||||||
|
restricted: true,
|
||||||
|
nav: "/logout",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "mdi-cog",
|
icon: "mdi-cog",
|
||||||
title: this.$i18n.t("general.settings"),
|
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: {
|
methods: {
|
||||||
navRouter(route) {
|
openLoginDialog() {
|
||||||
this.$router.push(route);
|
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>
|
|
|
@ -4,8 +4,9 @@
|
||||||
"take-me-home": "Take me Home"
|
"take-me-home": "Take me Home"
|
||||||
},
|
},
|
||||||
"new-recipe": {
|
"new-recipe": {
|
||||||
"from-url": "From URL",
|
"from-url": "Import a Recipe",
|
||||||
"recipe-url": "Recipe URL",
|
"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.",
|
"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",
|
"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"
|
"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"
|
||||||
|
@ -89,6 +90,8 @@
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"general-settings": "General Settings",
|
"general-settings": "General Settings",
|
||||||
|
"change-password": "Change Password",
|
||||||
|
"admin-settings": "Admin Settings",
|
||||||
"local-api": "Local API",
|
"local-api": "Local API",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"add-a-new-theme": "Add a New Theme",
|
"add-a-new-theme": "Add a New Theme",
|
||||||
|
|
|
@ -48,11 +48,11 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
import SuccessFailureAlert from "@/components/UI/SuccessFailureAlert";
|
||||||
import ImportSummaryDialog from "./ImportSummaryDialog";
|
import ImportSummaryDialog from "@/components/Admin/Backup/ImportSummaryDialog";
|
||||||
import UploadBtn from "../../UI/UploadBtn";
|
import UploadBtn from "@/components/UI/UploadBtn";
|
||||||
import AvailableBackupCard from "./AvailableBackupCard";
|
import AvailableBackupCard from "@/components/Admin/Backup/AvailableBackupCard";
|
||||||
import NewBackupCard from "./NewBackupCard";
|
import NewBackupCard from "@/components/Admin/Backup/NewBackupCard";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
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>
|
<script>
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import TimePickerDialog from "./TimePickerDialog";
|
import TimePickerDialog from "@/components/Admin/MealPlanner/TimePickerDialog";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
TimePickerDialog,
|
TimePickerDialog,
|
|
@ -13,35 +13,37 @@
|
||||||
{{ $t("migration.recipe-migration") }}
|
{{ $t("migration.recipe-migration") }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
</v-card>
|
|
||||||
|
|
||||||
<v-row dense>
|
<v-card-text>
|
||||||
<v-col
|
<v-row dense>
|
||||||
:cols="12"
|
<v-col
|
||||||
:sm="6"
|
:cols="12"
|
||||||
:md="6"
|
:sm="6"
|
||||||
:lg="4"
|
:md="6"
|
||||||
:xl="3"
|
:lg="4"
|
||||||
v-for="migration in migrations"
|
:xl="3"
|
||||||
:key="migration.title"
|
v-for="migration in migrations"
|
||||||
>
|
:key="migration.title"
|
||||||
<MigrationCard
|
>
|
||||||
:title="migration.title"
|
<MigrationCard
|
||||||
:folder="migration.urlVariable"
|
:title="migration.title"
|
||||||
:description="migration.description"
|
:folder="migration.urlVariable"
|
||||||
:available="migration.availableImports"
|
:description="migration.description"
|
||||||
@refresh="getAvailableMigrations"
|
:available="migration.availableImports"
|
||||||
@imported="showReport"
|
@refresh="getAvailableMigrations"
|
||||||
/>
|
@imported="showReport"
|
||||||
</v-col>
|
/>
|
||||||
</v-row>
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MigrationCard from "./MigrationCard";
|
import MigrationCard from "@/components/Admin/Migration/MigrationCard";
|
||||||
import SuccessFailureAlert from "../../UI/SuccessFailureAlert";
|
import SuccessFailureAlert from "@/components/UI/SuccessFailureAlert";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
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>
|
<template>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title>
|
<v-card-title class="headline">
|
||||||
{{ $t("settings.general-settings") }}
|
{{ $t("settings.admin-settings") }}
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<span>
|
<span>
|
||||||
<v-btn class="pt-1" text href="/docs">
|
<v-btn class="pt-1" text href="/docs">
|
||||||
|
@ -10,23 +10,7 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</span>
|
</span>
|
||||||
</v-card-title>
|
</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>
|
<v-divider></v-divider>
|
||||||
<HomePageSettings />
|
<HomePageSettings />
|
||||||
<v-divider></v-divider>
|
<v-divider></v-divider>
|
||||||
|
@ -34,7 +18,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import HomePageSettings from "./HomePageSettings";
|
import HomePageSettings from "@/components/Admin/General/HomePageSettings";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
|
@ -150,9 +150,9 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import ColorPickerDialog from "./ColorPickerDialog";
|
import ColorPickerDialog from "@/components/Admin/Theme/ColorPickerDialog";
|
||||||
import NewThemeDialog from "./NewThemeDialog";
|
import NewThemeDialog from "@/components/Admin/Theme/NewThemeDialog";
|
||||||
import Confirmation from "../../UI/Confirmation";
|
import Confirmation from "@/components/UI/Confirmation";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
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>
|
</v-img>
|
||||||
<br v-else />
|
<br v-else />
|
||||||
|
|
||||||
<ButtonRow
|
<EditorButtonRow
|
||||||
@json="jsonEditor = true"
|
@json="jsonEditor = true"
|
||||||
@editor="jsonEditor = false"
|
@editor="jsonEditor = false"
|
||||||
@save="createRecipe"
|
@save="createRecipe"
|
||||||
|
@ -43,12 +43,12 @@ import api from "@/api";
|
||||||
|
|
||||||
import RecipeEditor from "../components/Recipe/RecipeEditor";
|
import RecipeEditor from "../components/Recipe/RecipeEditor";
|
||||||
import VJsoneditor from "v-jsoneditor";
|
import VJsoneditor from "v-jsoneditor";
|
||||||
import ButtonRow from "../components/UI/ButtonRow";
|
import EditorButtonRow from "../components/Recipe/EditorButtonRow";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
VJsoneditor,
|
VJsoneditor,
|
||||||
RecipeEditor,
|
RecipeEditor,
|
||||||
ButtonRow,
|
EditorButtonRow,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
:performTime="recipeDetails.performTime"
|
:performTime="recipeDetails.performTime"
|
||||||
/>
|
/>
|
||||||
</v-img>
|
</v-img>
|
||||||
<ButtonRow
|
<EditorButtonRow
|
||||||
:open="showIcons"
|
:open="showIcons"
|
||||||
@json="jsonEditor = true"
|
@json="jsonEditor = true"
|
||||||
@editor="
|
@editor="
|
||||||
|
@ -62,14 +62,14 @@ import VJsoneditor from "v-jsoneditor";
|
||||||
import RecipeViewer from "../components/Recipe/RecipeViewer";
|
import RecipeViewer from "../components/Recipe/RecipeViewer";
|
||||||
import RecipeEditor from "../components/Recipe/RecipeEditor";
|
import RecipeEditor from "../components/Recipe/RecipeEditor";
|
||||||
import RecipeTimeCard from "../components/Recipe/RecipeTimeCard.vue";
|
import RecipeTimeCard from "../components/Recipe/RecipeTimeCard.vue";
|
||||||
import ButtonRow from "../components/UI/ButtonRow";
|
import EditorButtonRow from "../components/Recipe/EditorButtonRow";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
VJsoneditor,
|
VJsoneditor,
|
||||||
RecipeViewer,
|
RecipeViewer,
|
||||||
RecipeEditor,
|
RecipeEditor,
|
||||||
ButtonRow,
|
EditorButtonRow,
|
||||||
RecipeTimeCard,
|
RecipeTimeCard,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
|
@ -34,8 +34,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import SearchBar from "../components/UI/SearchBar";
|
import SearchBar from "../components/UI/Search/SearchBar";
|
||||||
import RecipeCard from "../components/UI/RecipeCard";
|
import RecipeCard from "../components/Recipe/RecipeCard";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SearchBar,
|
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 api from "@/api";
|
||||||
import Vuetify from "../../plugins/vuetify";
|
import Vuetify from "../../plugins/vuetify";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
function inDarkMode(payload) {
|
function inDarkMode(payload) {
|
||||||
let isDark;
|
let isDark;
|
||||||
|
@ -18,6 +19,8 @@ const state = {
|
||||||
activeTheme: {},
|
activeTheme: {},
|
||||||
darkMode: "system",
|
darkMode: "system",
|
||||||
isDark: false,
|
isDark: false,
|
||||||
|
isLoggedIn: false,
|
||||||
|
token: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
@ -35,6 +38,14 @@ const mutations = {
|
||||||
state.darkMode = payload;
|
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 = {
|
const actions = {
|
||||||
|
@ -47,6 +58,7 @@ const actions = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
async initTheme({ dispatch, getters }) {
|
async initTheme({ dispatch, getters }) {
|
||||||
//If theme is empty resetTheme
|
//If theme is empty resetTheme
|
||||||
if (Object.keys(getters.getActiveTheme).length === 0) {
|
if (Object.keys(getters.getActiveTheme).length === 0) {
|
||||||
|
@ -63,6 +75,8 @@ const getters = {
|
||||||
getActiveTheme: state => state.activeTheme,
|
getActiveTheme: state => state.activeTheme,
|
||||||
getDarkMode: state => state.darkMode,
|
getDarkMode: state => state.darkMode,
|
||||||
getIsDark: state => state.isDark,
|
getIsDark: state => state.isDark,
|
||||||
|
getIsLoggedIn: state => state.isLoggedIn,
|
||||||
|
getToken: state => state.token,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -20,11 +20,6 @@ const store = new Vuex.Store({
|
||||||
homePage,
|
homePage,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
// Home Page Settings
|
|
||||||
// Snackbar
|
|
||||||
snackActive: false,
|
|
||||||
snackText: "",
|
|
||||||
snackType: "warning",
|
|
||||||
|
|
||||||
// All Recipe Data Store
|
// All Recipe Data Store
|
||||||
recentRecipes: [],
|
recentRecipes: [],
|
||||||
|
@ -33,15 +28,6 @@ const store = new Vuex.Store({
|
||||||
},
|
},
|
||||||
|
|
||||||
mutations: {
|
mutations: {
|
||||||
setSnackBar(state, payload) {
|
|
||||||
state.snackText = payload.text;
|
|
||||||
state.snackType = payload.type;
|
|
||||||
state.snackActive = true;
|
|
||||||
},
|
|
||||||
setSnackActive(state, payload) {
|
|
||||||
state.snackActive = payload;
|
|
||||||
},
|
|
||||||
|
|
||||||
setRecentRecipes(state, payload) {
|
setRecentRecipes(state, payload) {
|
||||||
state.recentRecipes = payload;
|
state.recentRecipes = payload;
|
||||||
},
|
},
|
||||||
|
@ -68,11 +54,6 @@ const store = new Vuex.Store({
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
//
|
|
||||||
getSnackText: state => state.snackText,
|
|
||||||
getSnackActive: state => state.snackActive,
|
|
||||||
getSnackType: state => state.snackType,
|
|
||||||
|
|
||||||
getRecentRecipes: state => state.recentRecipes,
|
getRecentRecipes: state => state.recentRecipes,
|
||||||
getMealPlanCategories: state => state.mealPlanCategories,
|
getMealPlanCategories: state => state.mealPlanCategories,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const path = require("path");
|
||||||
module.exports = {
|
module.exports = {
|
||||||
transpileDependencies: ["vuetify"],
|
transpileDependencies: ["vuetify"],
|
||||||
publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
|
publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
|
||||||
|
@ -18,4 +19,11 @@ module.exports = {
|
||||||
enableInSFC: true,
|
enableInSFC: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
configureWebpack: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve("src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,9 @@ import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
# import utils.startup as startup
|
# 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 (
|
from routes import (
|
||||||
backup_routes,
|
backup_routes,
|
||||||
debug_routes,
|
debug_routes,
|
||||||
|
@ -17,8 +19,8 @@ from routes.recipe import (
|
||||||
recipe_crud_routes,
|
recipe_crud_routes,
|
||||||
tag_routes,
|
tag_routes,
|
||||||
)
|
)
|
||||||
from services.settings_services import default_settings_init
|
from routes.users import users
|
||||||
from utils.logger import logger
|
from fastapi.logger import logger
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Mealie",
|
title="Mealie",
|
||||||
|
@ -29,16 +31,17 @@ app = FastAPI(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def data_base_first_run():
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
|
||||||
def start_scheduler():
|
def start_scheduler():
|
||||||
import services.scheduler.scheduled_jobs
|
import services.scheduler.scheduled_jobs
|
||||||
|
|
||||||
|
|
||||||
def init_settings():
|
|
||||||
default_settings_init()
|
|
||||||
import services.theme_services
|
|
||||||
|
|
||||||
|
|
||||||
def api_routers():
|
def api_routers():
|
||||||
|
# Authentication
|
||||||
|
app.include_router(users.router)
|
||||||
# Recipes
|
# Recipes
|
||||||
app.include_router(all_recipe_routes.router)
|
app.include_router(all_recipe_routes.router)
|
||||||
app.include_router(category_routes.router)
|
app.include_router(category_routes.router)
|
||||||
|
@ -56,10 +59,11 @@ def api_routers():
|
||||||
app.include_router(debug_routes.router)
|
app.include_router(debug_routes.router)
|
||||||
|
|
||||||
|
|
||||||
|
if not sql_exists:
|
||||||
|
data_base_first_run()
|
||||||
|
|
||||||
api_routers()
|
api_routers()
|
||||||
start_scheduler()
|
start_scheduler()
|
||||||
init_settings()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logger.info("-----SYSTEM STARTUP-----")
|
logger.info("-----SYSTEM STARTUP-----")
|
||||||
|
|
|
@ -15,9 +15,12 @@ def ensure_dirs():
|
||||||
ENV = CWD.joinpath(".env")
|
ENV = CWD.joinpath(".env")
|
||||||
dotenv.load_dotenv(ENV)
|
dotenv.load_dotenv(ENV)
|
||||||
|
|
||||||
|
|
||||||
|
SECRET = "super-secret-key"
|
||||||
|
|
||||||
# General
|
# General
|
||||||
APP_VERSION = "v0.3.0"
|
APP_VERSION = "v0.3.0"
|
||||||
DB_VERSION = "v0.2.1"
|
DB_VERSION = "v0.3.0"
|
||||||
PRODUCTION = os.environ.get("ENV")
|
PRODUCTION = os.environ.get("ENV")
|
||||||
PORT = int(os.getenv("mealie_port", 9000))
|
PORT = int(os.getenv("mealie_port", 9000))
|
||||||
API = os.getenv("api_docs", True)
|
API = os.getenv("api_docs", True)
|
||||||
|
@ -30,7 +33,7 @@ else:
|
||||||
redoc_url = None
|
redoc_url = None
|
||||||
|
|
||||||
# Helpful Globals
|
# Helpful Globals
|
||||||
DATA_DIR = CWD.parent.joinpath("app_data")
|
DATA_DIR = CWD.parent.parent.joinpath("app_data")
|
||||||
if PRODUCTION:
|
if PRODUCTION:
|
||||||
DATA_DIR = Path("/app/data")
|
DATA_DIR = Path("/app/data")
|
||||||
|
|
||||||
|
@ -59,6 +62,8 @@ REQUIRED_DIRS = [
|
||||||
|
|
||||||
ensure_dirs()
|
ensure_dirs()
|
||||||
|
|
||||||
|
LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
|
||||||
|
|
||||||
|
|
||||||
# DATABASE ENV
|
# DATABASE ENV
|
||||||
SQLITE_FILE = None
|
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 sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from db.db_base import BaseDocument
|
from db.db_base import BaseDocument
|
||||||
from db.sql.meal_models import MealPlanModel
|
from db.models.mealplan import MealPlanModel
|
||||||
from db.sql.recipe_models import Category, RecipeModel, Tag
|
from db.models.recipe import Category, RecipeModel, Tag
|
||||||
from db.sql.settings_models import SiteSettingsModel
|
from db.models.settings import SiteSettingsModel
|
||||||
from db.sql.theme_models import SiteThemeModel
|
from db.models.theme import SiteThemeModel
|
||||||
|
from db.models.users import User
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# TODO
|
# TODO
|
||||||
|
@ -55,6 +56,12 @@ class _Themes(BaseDocument):
|
||||||
self.sql_model = SiteThemeModel
|
self.sql_model = SiteThemeModel
|
||||||
|
|
||||||
|
|
||||||
|
class _Users(BaseDocument):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.primary_key = "id"
|
||||||
|
self.sql_model = User
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.recipes = _Recipes()
|
self.recipes = _Recipes()
|
||||||
|
@ -63,6 +70,7 @@ class Database:
|
||||||
self.themes = _Themes()
|
self.themes = _Themes()
|
||||||
self.categories = _Categories()
|
self.categories = _Categories()
|
||||||
self.tags = _Tags()
|
self.tags = _Tags()
|
||||||
|
self.users = _Users()
|
||||||
|
|
||||||
|
|
||||||
db = Database()
|
db = Database()
|
||||||
|
|
|
@ -3,7 +3,7 @@ from typing import List
|
||||||
from sqlalchemy.orm import load_only
|
from sqlalchemy.orm import load_only
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from db.sql.model_base import SqlAlchemyBase
|
from db.models.model_base import SqlAlchemyBase
|
||||||
|
|
||||||
|
|
||||||
class BaseDocument:
|
class BaseDocument:
|
||||||
|
@ -108,7 +108,10 @@ class BaseDocument:
|
||||||
db_entries = [x.dict() for x in result]
|
db_entries = [x.dict() for x in result]
|
||||||
|
|
||||||
if limit == 1:
|
if limit == 1:
|
||||||
return db_entries[0]
|
try:
|
||||||
|
return db_entries[0]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
return db_entries
|
return db_entries
|
||||||
|
|
||||||
|
@ -124,9 +127,8 @@ class BaseDocument:
|
||||||
"""
|
"""
|
||||||
new_document = self.sql_model(session=session, **document)
|
new_document = self.sql_model(session=session, **document)
|
||||||
session.add(new_document)
|
session.add(new_document)
|
||||||
return_data = new_document.dict()
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
return_data = new_document.dict()
|
||||||
return return_data
|
return return_data
|
||||||
|
|
||||||
def update(self, session: Session, match_value: str, new_data: str) -> dict:
|
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 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
|
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
|
from pathlib import Path
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from db.sql.model_base import SqlAlchemyBase
|
from db.models.model_base import SqlAlchemyBase
|
||||||
from sqlalchemy.orm import sessionmaker
|
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)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
import db.sql._all_models
|
import db.models._all_models
|
||||||
|
|
||||||
SqlAlchemyBase.metadata.create_all(engine)
|
SqlAlchemyBase.metadata.create_all(engine)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from typing import List
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
|
||||||
|
|
||||||
class Meal(SqlAlchemyBase):
|
class Meal(SqlAlchemyBase):
|
|
@ -4,11 +4,11 @@ from typing import List
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
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 slugify import slugify
|
||||||
from sqlalchemy.ext.orderinglist import ordering_list
|
from sqlalchemy.ext.orderinglist import ordering_list
|
||||||
from sqlalchemy.orm import validates
|
from sqlalchemy.orm import validates
|
||||||
from utils.logger import logger
|
from fastapi.logger import logger
|
||||||
|
|
||||||
|
|
||||||
class ApiExtras(SqlAlchemyBase):
|
class ApiExtras(SqlAlchemyBase):
|
|
@ -1,7 +1,6 @@
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||||
from db.sql.recipe_models import Category
|
|
||||||
|
|
||||||
|
|
||||||
class SiteSettingsModel(SqlAlchemyBase, BaseMixins):
|
class SiteSettingsModel(SqlAlchemyBase, BaseMixins):
|
|
@ -1,6 +1,6 @@
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
from db.sql.model_base import BaseMixins, SqlAlchemyBase
|
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
|
||||||
|
|
||||||
class SiteThemeModel(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 operator
|
||||||
import shutil
|
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 db.db_setup import generate_session
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
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.exports import backup_all
|
||||||
from services.backups.imports import ImportDatabase
|
from services.backups.imports import ImportDatabase
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from starlette.responses import FileResponse
|
from starlette.responses import FileResponse
|
||||||
from utils.snackbar import SnackResponse
|
from schema.snackbar import SnackResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/backups", tags=["Backups"])
|
router = APIRouter(prefix="/api/backups", tags=["Backups"])
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import json
|
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 import APIRouter
|
||||||
from fastapi.responses import HTMLResponse
|
|
||||||
from utils.logger import LOGGER_FILE
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/debug", tags=["Debug"])
|
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 fastapi import APIRouter, Depends, HTTPException
|
||||||
from services.meal_services import MealPlan
|
from services.meal_services import MealPlan
|
||||||
from sqlalchemy.orm.session import Session
|
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"])
|
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,14 @@ import operator
|
||||||
import shutil
|
import shutil
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from app_config import MIGRATION_DIR
|
from core.config import MIGRATION_DIR
|
||||||
from db.db_setup import generate_session
|
from db.db_setup import generate_session
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
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.chowdown import chowdown_migrate as chowdow_migrate
|
||||||
from services.migrations.nextcloud import migrate as nextcloud_migrate
|
from services.migrations.nextcloud import migrate as nextcloud_migrate
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from utils.snackbar import SnackResponse
|
from schema.snackbar import SnackResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/migrations", tags=["Migration"])
|
router = APIRouter(prefix="/api/migrations", tags=["Migration"])
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ from typing import List, Optional
|
||||||
from db.database import db
|
from db.database import db
|
||||||
from db.db_setup import generate_session
|
from db.db_setup import generate_session
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
from models.recipe_models import AllRecipeRequest
|
from schema.recipe import AllRecipeRequest
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
from db.database import db
|
from db.database import db
|
||||||
from db.db_setup import generate_session
|
from db.db_setup import generate_session
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from models.category_models import RecipeCategoryResponse
|
from schema.category import RecipeCategoryResponse
|
||||||
from sqlalchemy.orm.session import Session
|
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(
|
router = APIRouter(
|
||||||
prefix="/api/categories",
|
prefix="/api/categories",
|
||||||
|
@ -27,7 +27,6 @@ def get_all_recipes_by_category(
|
||||||
return db.categories.get(session, category)
|
return db.categories.get(session, category)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{category}")
|
@router.delete("/{category}")
|
||||||
async def delete_recipe_category(
|
async def delete_recipe_category(
|
||||||
category: str, session: Session = Depends(generate_session)
|
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 import APIRouter, Depends, File, Form, HTTPException
|
||||||
from fastapi.logger import logger
|
from fastapi.logger import logger
|
||||||
from fastapi.responses import FileResponse
|
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.image_services import read_image, write_image
|
||||||
from services.recipe_services import Recipe
|
from services.recipe_services import Recipe
|
||||||
from services.scraper.scraper import create_from_url
|
from services.scraper.scraper import create_from_url
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from utils.snackbar import SnackResponse
|
from schema.snackbar import SnackResponse
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/api/recipes",
|
prefix="/api/recipes",
|
||||||
|
|
|
@ -2,9 +2,9 @@ from db.database import db
|
||||||
from db.db_setup import generate_session
|
from db.db_setup import generate_session
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.orm.session import Session
|
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"])
|
router = APIRouter(tags=["Recipes"])
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
from db.database import db
|
from db.database import db
|
||||||
from db.db_setup import generate_session
|
from db.db_setup import generate_session
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from models.settings_models import SiteSettings
|
from schema.settings import SiteSettings
|
||||||
from services.settings_services import default_settings_init
|
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from utils.post_webhooks import post_webhooks
|
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"])
|
router = APIRouter(prefix="/api/site-settings", tags=["Settings"])
|
||||||
|
|
||||||
|
@ -17,8 +16,7 @@ def get_main_settings(session: Session = Depends(generate_session)):
|
||||||
try:
|
try:
|
||||||
data = db.settings.get(session, "main")
|
data = db.settings.get(session, "main")
|
||||||
except:
|
except:
|
||||||
default_settings_init(session)
|
return
|
||||||
data = db.settings.get(session, "main")
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
from db.db_setup import generate_session
|
from db.db_setup import generate_session
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from models.theme_models import SiteTheme
|
from schema.theme import SiteTheme
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from utils.snackbar import SnackResponse
|
from schema.snackbar import SnackResponse
|
||||||
from db.database import db
|
from db.database import db
|
||||||
|
|
||||||
router = APIRouter(prefix="/api", tags=["Themes"])
|
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 datetime import datetime
|
||||||
from pathlib import Path
|
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.database import db
|
||||||
from db.db_setup import create_session
|
from db.db_setup import create_session
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
from services.meal_services import MealPlan
|
from services.meal_services import MealPlan
|
||||||
from services.recipe_services import Recipe
|
from services.recipe_services import Recipe
|
||||||
from utils.logger import logger
|
from fastapi.logger import logger
|
||||||
|
|
||||||
|
|
||||||
class ExportDatabase:
|
class ExportDatabase:
|
||||||
|
|
|
@ -4,13 +4,13 @@ import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
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 db.database import db
|
||||||
from models.import_models import RecipeImport, SettingsImport, ThemeImport
|
from schema.restore import RecipeImport, SettingsImport, ThemeImport
|
||||||
from models.theme_models import SiteTheme
|
from schema.theme import SiteTheme
|
||||||
from services.recipe_services import Recipe
|
from services.recipe_services import Recipe
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from utils.logger import logger
|
from fastapi.logger import logger
|
||||||
|
|
||||||
|
|
||||||
class ImportDatabase:
|
class ImportDatabase:
|
||||||
|
|
|
@ -2,8 +2,8 @@ import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from app_config import IMG_DIR
|
from core.config import IMG_DIR
|
||||||
from utils.logger import logger
|
from fastapi.logger import logger
|
||||||
|
|
||||||
|
|
||||||
def read_image(recipe_slug: str) -> Path:
|
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