mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
Feature/authentication (#190)
* 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 * password reset * store refactor * dependency update * abstract button props * profile page refactor * basic password validation * login form refactor/split v-container * remo unused code * hide editor buttons when not logged in * mkdocs dev dependency * v0.4.0 docs update * profile image upload * additional token routes * Smaller recipe cards for smaller viewports * fix admin sidebar * add users * change to outlined Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
68b6a3a256
commit
9af664c259
57 changed files with 1365 additions and 462 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -14,6 +14,7 @@ app_data/backups/*
|
|||
app_data/debug/*
|
||||
app_data/img/*
|
||||
app_data/migration/*
|
||||
app_data/users/*
|
||||
|
||||
#Exception to keep folders
|
||||
!mealie/dist/.gitkeep
|
||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"python.formatting.provider": "black",
|
||||
"python.pythonPath": ".venv/bin/python3.9",
|
||||
"python.pythonPath": "./.venv/bin/python3.9",
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.autoComplete.extraPaths": ["mealie", "mealie/mealie"],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Site Settings Panel
|
||||
!!! danger
|
||||
As this is still a **BETA** It is recommended that you backup your data often and store in more than one place. Ad-hear to backup best practices with the [3-2-1 Backup Rule](https://en.wikipedia.org/wiki/Backup)
|
||||
As this is still a **BETA** It is recommended that you backup your data often and store in more than one place. Ad-hear to backup best practices with the [3-2-1 Backup Rule](https://en.wikipedia.org/wiki/Backup). Prior to upgrading you **should** perform a backup to limit any data loss.
|
||||
|
||||
## General Settings
|
||||
In your site settings page you can select several options to change the layout of your homepage. You can choose to display the recent recipes, how many cards to show for each section, and which category sections to display. You can additionally select which language to use by default. Note the currently homepage settings are saved in your browser. In the future a database entry will be made for site settings so the homepage is consistent across users.
|
||||
|
@ -12,7 +12,7 @@ Color themes can be created and set from the UI in the settings page. You can se
|
|||
|
||||

|
||||
|
||||
!!! note
|
||||
!!! tip
|
||||
Theme data is stored in localstorage in the browser. Calling "Save colors and apply theme will refresh the local storage with the selected theme as well save the theme to the database.
|
||||
|
||||
|
||||
|
|
32
docs/docs/getting-started/users.md
Normal file
32
docs/docs/getting-started/users.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# User Managemenet
|
||||
|
||||
## Overview
|
||||
The basic relationship and ownership of recipes and meal plans is based on a user and group model where users are owners of recipes and groups are owners of meal plans. By default all users will be added to the default group. If a recipe is added through a migration or through a backup where no user exists ownership will be set to the default Admin, the original user provided by Mealie. To fully understand how to structure your users and groups, you'll need to know how each role is used in the website.
|
||||
|
||||
!!! info ":fontawesome-solid-user-cog: Admins"
|
||||
Mealie admins are super users that have access to all user data (excluding passwords). All admins can perform administrative tasks like adding users, resetting user passwords, backing up the database, migrating data, and managing site settings. Administrators can also access restricted recipes that are marked hidden or with editing disabled by the owner.
|
||||
|
||||
!!! info ":fontawesome-solid-users: User Groups"
|
||||
User groups, or "family" groups are a collection of users that are associated together. Users belonging to groups will have access to their associated meal plans and associated pages. This is currently the only feature of groups.
|
||||
|
||||
!!! info ":fontawesome-solid-user: User"
|
||||
A single user created by an Admin that has basic privileges to edit their profile, create and edit recipes they own. Edit recipes that are not hidden and are marked editable.
|
||||
|
||||
## Startup
|
||||
On the first startup you'll need to login to Mealie using the default username and password `changeme@email.com` and `MyPassword` or the default set through the env variable. On first login you'll be required to reset your password. After resetting your password you should also change your email address as appropriate. This will be used for logins on all future requests.
|
||||
|
||||
!!! tip
|
||||
Your default password environmental variable will be the default password for all new users that are created. This is stored in plain text and should not be used **any where** else.
|
||||
|
||||
|
||||
## Creating and Editing Users
|
||||
// TODO
|
||||
|
||||
## Creating Groups
|
||||
// TODO
|
||||
|
||||
## Password Reset
|
||||
// TODO
|
||||
|
||||
## Examples Use Cases
|
||||
// TODO
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 24 KiB |
BIN
docs/docs/img/app_diagram.png
Normal file
BIN
docs/docs/img/app_diagram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 138 KiB |
|
@ -29,6 +29,7 @@ nav:
|
|||
- Getting Started:
|
||||
- Installation: "getting-started/install.md"
|
||||
- Working With Recipes: "getting-started/recipes.md"
|
||||
- User Management: "getting-started/users.md"
|
||||
- Planning Meals: "getting-started/meal-planner.md"
|
||||
- Site Settings: "getting-started/site-settings.md"
|
||||
- Backups and Exports: "getting-started/backups-and-exports.md"
|
||||
|
|
178
frontend/package-lock.json
generated
178
frontend/package-lock.json
generated
|
@ -23,16 +23,6 @@
|
|||
"markdown-it-sup": "^1.0.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"markdown-it-toc-and-anchor": "^4.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"markdown-it-katex": {
|
||||
"version": "npm:@iktakahiro/markdown-it-katex@4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@iktakahiro/markdown-it-katex/-/markdown-it-katex-4.0.1.tgz",
|
||||
"integrity": "sha512-kGFooO7fIOgY34PSG8ZNVsUlKhhNoqhzW2kq94TNGa8COzh73PO4KsEoPOsQVG1mEAe8tg7GqG0FoVao0aMHaw==",
|
||||
"requires": {
|
||||
"katex": "^0.12.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
|
@ -2016,6 +2006,16 @@
|
|||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"cacache": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz",
|
||||
|
@ -2042,6 +2042,53 @@
|
|||
"unique-filename": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
|
@ -2058,6 +2105,16 @@
|
|||
"minipass": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"terser-webpack-plugin": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
|
||||
|
@ -2074,6 +2131,18 @@
|
|||
"terser": "^4.6.12",
|
||||
"webpack-sources": "^1.4.3"
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
|
||||
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -7551,6 +7620,14 @@
|
|||
"resolved": "https://registry.npmjs.org/markdown-it-ins/-/markdown-it-ins-3.0.1.tgz",
|
||||
"integrity": "sha512-32SSfZqSzqyAmmQ4SHvhxbFqSzPDqsZgMHDwxqPzp+v+t8RsmqsBZRG+RfRQskJko9PfKC2/oxyOs4Yg/CfiRw=="
|
||||
},
|
||||
"markdown-it-katex": {
|
||||
"version": "npm:@iktakahiro/markdown-it-katex@4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@iktakahiro/markdown-it-katex/-/markdown-it-katex-4.0.1.tgz",
|
||||
"integrity": "sha512-kGFooO7fIOgY34PSG8ZNVsUlKhhNoqhzW2kq94TNGa8COzh73PO4KsEoPOsQVG1mEAe8tg7GqG0FoVao0aMHaw==",
|
||||
"requires": {
|
||||
"katex": "^0.12.0"
|
||||
}
|
||||
},
|
||||
"markdown-it-mark": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz",
|
||||
|
@ -11876,87 +11953,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
|
||||
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "3.4.9",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz",
|
||||
|
|
|
@ -31,10 +31,10 @@
|
|||
<LanguageMenu />
|
||||
</v-app-bar>
|
||||
<v-main>
|
||||
<v-container>
|
||||
<AddRecipeFab />
|
||||
<router-view></router-view>
|
||||
</v-container>
|
||||
<v-slide-x-reverse-transition>
|
||||
<AddRecipeFab v-if="loggedIn" />
|
||||
</v-slide-x-reverse-transition>
|
||||
<router-view></router-view>
|
||||
<FlashMessage :position="'right bottom'"></FlashMessage>
|
||||
</v-main>
|
||||
</v-app>
|
||||
|
@ -46,6 +46,7 @@ import SearchBar from "@/components/UI/Search/SearchBar";
|
|||
import AddRecipeFab from "@/components/UI/AddRecipeFab";
|
||||
import LanguageMenu from "@/components/UI/LanguageMenu";
|
||||
import Vuetify from "./plugins/vuetify";
|
||||
import { user } from "@/mixins/user";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
|
@ -57,6 +58,8 @@ export default {
|
|||
LanguageMenu,
|
||||
},
|
||||
|
||||
mixins: [user],
|
||||
|
||||
watch: {
|
||||
$route() {
|
||||
this.search = false;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const baseURL = "/api/";
|
||||
import axios from "axios";
|
||||
import utils from "@/utils";
|
||||
import { store } from "../store/store";
|
||||
import { store } from "../store";
|
||||
|
||||
axios.defaults.headers.common[
|
||||
"Authorization"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
import { store } from "../store/store";
|
||||
import { store } from "../store";
|
||||
|
||||
const backupBase = baseURL + "backups/";
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ const prefix = baseURL + "categories";
|
|||
|
||||
const categoryURLs = {
|
||||
get_all: `${prefix}`,
|
||||
get_category: (category) => `${prefix}/${category}`,
|
||||
delete_category: (category) => `${prefix}/${category}`,
|
||||
get_category: category => `${prefix}/${category}`,
|
||||
delete_category: category => `${prefix}/${category}`,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
import { store } from "../store/store";
|
||||
import { store } from "../store";
|
||||
|
||||
const migrationBase = baseURL + "migrations";
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
import { store } from "../store/store";
|
||||
import { store } from "../store";
|
||||
import { router } from "../main";
|
||||
import qs from "qs";
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ const usersURLs = {
|
|||
users: `${userPrefix}`,
|
||||
self: `${userPrefix}/self`,
|
||||
userID: id => `${userPrefix}/${id}`,
|
||||
password: id => `${userPrefix}/${id}/password`,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -42,6 +43,10 @@ export default {
|
|||
let response = await apiReq.put(usersURLs.userID(user.id), user);
|
||||
return response.data;
|
||||
},
|
||||
async changePassword(id, password) {
|
||||
let response = await apiReq.put(usersURLs.password(id), password);
|
||||
return response.data;
|
||||
},
|
||||
async delete(id) {
|
||||
let response = await apiReq.delete(usersURLs.userID(id));
|
||||
return response.data;
|
||||
|
|
|
@ -22,13 +22,22 @@
|
|||
>
|
||||
<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 color="accent" class="white--text">
|
||||
<img
|
||||
:src="userProfileImage"
|
||||
v-if="!hideImage"
|
||||
@error="hideImage = true"
|
||||
/>
|
||||
<div v-else>
|
||||
{{ initials }}
|
||||
</div>
|
||||
</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-title> {{ user.fullName }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ user.admin ? "Admin" : "User" }}</v-list-item-subtitle
|
||||
>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
@ -50,7 +59,7 @@
|
|||
</v-list>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-list nav dense>
|
||||
<v-list nav dense v-if="user.admin">
|
||||
<v-list-item
|
||||
v-for="nav in superLinks"
|
||||
:key="nav.title"
|
||||
|
@ -68,9 +77,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { validators } from "@/mixins/validators";
|
||||
import { initials } from "@/mixins/initials";
|
||||
import { user } from "@/mixins/user";
|
||||
export default {
|
||||
mixins: [validators, initials, user],
|
||||
data() {
|
||||
return {
|
||||
hideImage: false,
|
||||
showSidebar: false,
|
||||
mobile: false,
|
||||
links: [],
|
||||
|
@ -115,11 +129,17 @@ export default {
|
|||
],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
this.mobile = this.viewScale();
|
||||
this.showSidebar = !this.viewScale();
|
||||
},
|
||||
|
||||
computed: {
|
||||
userProfileImage() {
|
||||
return `api/users/${this.user.id}/image`;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
viewScale() {
|
||||
switch (this.$vuetify.breakpoint.name) {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<template>
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<h2 class="mt-1 mb-1">{{$t('settings.homepage.home-page')}}</h2>
|
||||
<h2 class="mt-1 mb-1">{{ $t("settings.homepage.home-page") }}</h2>
|
||||
<v-row align="center" justify="center" dense class="mb-n7 pb-n5">
|
||||
<v-col cols="12" sm="3" md="2">
|
||||
<v-switch v-model="showRecent" :label="$t('settings.homepage.show-recent')"></v-switch>
|
||||
<v-switch
|
||||
v-model="showRecent"
|
||||
:label="$t('settings.homepage.show-recent')"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="5" md="5">
|
||||
<v-slider
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
</v-card>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-card class="text-center ma-2">
|
||||
<v-card outlined class="text-center ma-2">
|
||||
<v-card-text>
|
||||
{{ $t("migration.no-migration-data-available") }}
|
||||
</v-card-text>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="500">
|
||||
<v-dialog v-model="dialog" width="500px">
|
||||
<LoginForm @logged-in="dialog = false" />
|
||||
</v-dialog>
|
||||
</div>
|
||||
|
|
|
@ -1,93 +1,65 @@
|
|||
<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-card width="500px">
|
||||
<v-divider></v-divider>
|
||||
<v-app-bar dark color="primary" class="mt-n1 mb-4">
|
||||
<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-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"
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
v-if="!options.isLoggingIn"
|
||||
v-model="user.name"
|
||||
light="light"
|
||||
@click="options.isLoggingIn = false"
|
||||
prepend-icon="person"
|
||||
:label="$t('general.name')"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.email"
|
||||
light="light"
|
||||
prepend-icon="mdi-email"
|
||||
validate-on-blur
|
||||
:label="$t('login.email')"
|
||||
type="email"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.password"
|
||||
light="light"
|
||||
class="mb-2s"
|
||||
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-btn
|
||||
v-if="options.isLoggingIn"
|
||||
@click.prevent="login"
|
||||
dark
|
||||
color="primary"
|
||||
block="block"
|
||||
type="submit"
|
||||
>{{ $t("login.sign-in") }}</v-btn
|
||||
>
|
||||
Sign up
|
||||
</v-btn>
|
||||
</v-card-actions> -->
|
||||
</v-card>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -128,13 +100,18 @@ export default {
|
|||
} catch {
|
||||
this.error = true;
|
||||
}
|
||||
if (key.status != 200) this.error = true;
|
||||
else {
|
||||
this.$emit("logged-in");
|
||||
if (key.status != 200) {
|
||||
this.error = true;
|
||||
this.loading = false;
|
||||
} else {
|
||||
this.clear();
|
||||
this.$store.commit("setToken", key.data.access_token);
|
||||
this.$emit("logged-in");
|
||||
}
|
||||
console.log(key);
|
||||
this.$store.commit("setToken", key.data.access_token)
|
||||
|
||||
let user = await api.users.self();
|
||||
this.$store.commit("setUserData", user);
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -76,7 +76,6 @@
|
|||
<v-btn color="success" @click="save" text :disabled="meals.length == 0">
|
||||
{{ $t("general.save") }}
|
||||
</v-btn>
|
||||
|
||||
</v-card-actions>
|
||||
</v-row>
|
||||
</v-card>
|
||||
|
|
62
frontend/src/components/Recipe/MobileRecipeCard.vue
Normal file
62
frontend/src/components/Recipe/MobileRecipeCard.vue
Normal file
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<v-card :to="`/recipe/${slug}`" max-height="125">
|
||||
<v-list-item>
|
||||
<v-list-item-avatar rounded size="125" class="mt-0 ml-n4">
|
||||
<v-img :src="getImage(image)"> </v-img>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ name }}
|
||||
</v-list-item-title>
|
||||
<v-rating length="5" size="16" dense :value="rating"></v-rating>
|
||||
<div class="text">
|
||||
<v-list-item-action-text>
|
||||
{{ description | truncate(50) }}
|
||||
</v-list-item-action-text>
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "@/utils";
|
||||
export default {
|
||||
props: {
|
||||
name: String,
|
||||
slug: String,
|
||||
description: String,
|
||||
rating: Number,
|
||||
image: String,
|
||||
route: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-card--reveal {
|
||||
align-items: center;
|
||||
bottom: 0;
|
||||
justify-content: center;
|
||||
opacity: 0.8;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
.v-card--text-show {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.headerClass {
|
||||
white-space: nowrap;
|
||||
word-break: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
|
@ -31,7 +31,7 @@
|
|||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-row>
|
||||
<v-row v-if="!viewScale">
|
||||
<v-col
|
||||
:sm="6"
|
||||
:md="6"
|
||||
|
@ -49,14 +49,36 @@
|
|||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else dense>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
xl="3"
|
||||
v-for="recipe in recipes.slice(0, cardLimit)"
|
||||
:key="recipe.name"
|
||||
|
||||
>
|
||||
<MobileRecipeCard
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RecipeCard from "../Recipe/RecipeCard";
|
||||
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
RecipeCard,
|
||||
MobileRecipeCard,
|
||||
},
|
||||
props: {
|
||||
sortable: {
|
||||
|
@ -68,8 +90,17 @@ export default {
|
|||
default: 6,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
computed: {
|
||||
viewScale() {
|
||||
switch (this.$vuetify.breakpoint.name) {
|
||||
case "xs":
|
||||
return true;
|
||||
case "sm":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<v-form ref="file">
|
||||
<input ref="uploader" class="d-none" type="file" @change="onFileChanged" />
|
||||
<v-btn :loading="isSelecting" @click="onButtonClick" color="accent" text>
|
||||
<v-icon left> mdi-cloud-upload </v-icon>
|
||||
{{ $t("general.upload") }}
|
||||
<v-icon left> {{ icon }}</v-icon>
|
||||
{{ text ? text : defaultText }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
|
@ -13,18 +13,27 @@ import api from "@/api";
|
|||
export default {
|
||||
props: {
|
||||
url: String,
|
||||
text: { default: "Upload" },
|
||||
icon: { default: "mdi-cloud-upload" },
|
||||
fileName: { defaul: "archive" },
|
||||
},
|
||||
data: () => ({
|
||||
file: null,
|
||||
isSelecting: false,
|
||||
}),
|
||||
|
||||
computed: {
|
||||
defaultText() {
|
||||
return this.$t("general.upload");
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async upload() {
|
||||
if (this.file != null) {
|
||||
this.isSelecting = true;
|
||||
let formData = new FormData();
|
||||
formData.append("archive", this.file);
|
||||
formData.append(this.fileName, this.file);
|
||||
|
||||
await api.utils.uploadFile(this.url, formData);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Vue from "vue";
|
||||
import App from "./App.vue";
|
||||
import vuetify from "./plugins/vuetify";
|
||||
import store from "./store/store";
|
||||
import store from "./store";
|
||||
import VueRouter from "vue-router";
|
||||
import { routes } from "./routes";
|
||||
import i18n from "./i18n";
|
||||
|
|
17
frontend/src/mixins/initials.js
Normal file
17
frontend/src/mixins/initials.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
export const initials = {
|
||||
computed: {
|
||||
initials() {
|
||||
const allNames = this.user.fullName.trim().split(" ");
|
||||
const initials = allNames.reduce(
|
||||
(acc, curr, index) => {
|
||||
if (index === 0 || index === allNames.length - 1) {
|
||||
acc = `${acc}${curr.charAt(0).toUpperCase()}`;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[""]
|
||||
);
|
||||
return initials;
|
||||
},
|
||||
},
|
||||
};
|
24
frontend/src/mixins/user.js
Normal file
24
frontend/src/mixins/user.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { store } from "@/store";
|
||||
export const user = {
|
||||
computed: {
|
||||
user() {
|
||||
return store.getters.getUserData;
|
||||
},
|
||||
loggedIn() {
|
||||
return store.getters.getIsLoggedIn;
|
||||
},
|
||||
initials() {
|
||||
const allNames = this.user.fullName.trim().split(" ");
|
||||
const initials = allNames.reduce(
|
||||
(acc, curr, index) => {
|
||||
if (index === 0 || index === allNames.length - 1) {
|
||||
acc = `${acc}${curr.charAt(0).toUpperCase()}`;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[""]
|
||||
);
|
||||
return initials;
|
||||
},
|
||||
},
|
||||
};
|
15
frontend/src/mixins/validators.js
Normal file
15
frontend/src/mixins/validators.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
export const validators = {
|
||||
data() {
|
||||
return {
|
||||
emailRule: v =>
|
||||
!v ||
|
||||
/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(v) ||
|
||||
"E-mail must be valid",
|
||||
|
||||
existsRule: value => !!value || "Field Required",
|
||||
|
||||
minRule: v =>
|
||||
v.length >= 8 || "Use 8 characters or more for your password",
|
||||
};
|
||||
},
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<v-container class="text-center">
|
||||
<v-row>
|
||||
<v-col cols="2"></v-col>
|
||||
<v-col>
|
||||
|
@ -12,7 +12,7 @@
|
|||
</v-col>
|
||||
<v-col cols="2"></v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -33,18 +33,22 @@
|
|||
</v-app-bar>
|
||||
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-form ref="newUser">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.fullName"
|
||||
label="Full Name"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.email"
|
||||
label="Email"
|
||||
:rules="[existsRule, emailRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
|
@ -57,6 +61,7 @@
|
|||
<v-text-field
|
||||
v-model="editedItem.password"
|
||||
label="User Password"
|
||||
:rules="[existsRule, minRule]"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
|
@ -66,7 +71,7 @@
|
|||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
|
@ -121,8 +126,10 @@
|
|||
<script>
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
import api from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
export default {
|
||||
components: { Confirmation },
|
||||
mixins: [validators],
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
activeId: null,
|
||||
|
@ -145,6 +152,7 @@ export default {
|
|||
editedItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
|
@ -152,6 +160,7 @@ export default {
|
|||
defaultItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
|
@ -186,7 +195,7 @@ export default {
|
|||
},
|
||||
|
||||
async deleteUser() {
|
||||
await api.users.delete(this.editedIndex);
|
||||
await api.users.delete(this.activeId);
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
|
@ -227,13 +236,13 @@ export default {
|
|||
|
||||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
console.log("New User", this.editedItem);
|
||||
api.users.update(this.editedItem);
|
||||
} else {
|
||||
this.close();
|
||||
} else if (this.$refs.newUser.validate()) {
|
||||
api.users.create(this.editedItem);
|
||||
this.close();
|
||||
}
|
||||
await this.initialize();
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,59 +1,168 @@
|
|||
<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>
|
||||
<v-row dense>
|
||||
<v-col cols="12" md="8" sm="12">
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<span>
|
||||
<v-progress-circular
|
||||
v-if="loading"
|
||||
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-row>
|
||||
<v-col cols="12" md="3" align="center" justify="center">
|
||||
<v-avatar
|
||||
color="accent"
|
||||
size="120"
|
||||
class="white--text headline mr-2"
|
||||
>
|
||||
<img
|
||||
:src="userProfileImage"
|
||||
v-if="!hideImage"
|
||||
@error="hideImage = true"
|
||||
/>
|
||||
<div v-else>
|
||||
{{ initials }}
|
||||
</div>
|
||||
</v-avatar>
|
||||
</v-col>
|
||||
<v-col cols="12" md="9">
|
||||
<v-form>
|
||||
<v-text-field
|
||||
label="Full Name"
|
||||
required
|
||||
v-model="user.fullName"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
label="Email"
|
||||
:rules="[emailRule]"
|
||||
validate-on-blur
|
||||
required
|
||||
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-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<UploadBtn
|
||||
icon="mdi-image-area"
|
||||
text="Upload Photo"
|
||||
:url="userProfileImage"
|
||||
file-name="profile_image"
|
||||
/>
|
||||
|
||||
<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>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="12">
|
||||
<v-card height="100%">
|
||||
<v-card-title class="headline">
|
||||
Reset Password
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-form ref="passChange">
|
||||
<v-text-field
|
||||
v-model="password.current"
|
||||
prepend-icon="mdi-lock"
|
||||
label="Current Password"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
@click:append="showPassword.current = !showPassword.current"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="password.newOne"
|
||||
prepend-icon="mdi-lock"
|
||||
label="New Password"
|
||||
:rules="[minRule]"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
@click:append="showPassword.newOne = !showPassword.newOne"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="password.newTwo"
|
||||
prepend-icon="mdi-lock"
|
||||
label="Confirm Password"
|
||||
:rules="[
|
||||
password.newOne === password.newTwo || 'Password must match',
|
||||
]"
|
||||
validate-on-blur
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
@click:append="showPassword.newTwo = !showPassword.newTwo"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
icon
|
||||
@click="showPassword = !showPassword"
|
||||
:loading="passwordLoading"
|
||||
>
|
||||
<v-icon v-if="!showPassword">mdi-eye-off</v-icon>
|
||||
<v-icon v-else> mdi-eye </v-icon>
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="accent" class="mr-2" @click="changePassword">
|
||||
<v-icon left> mdi-lock </v-icon>
|
||||
{{ $t("settings.change-password") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import AvatarPicker from '@/components/AvatarPicker'
|
||||
import UploadBtn from "@/components/UI/UploadBtn";
|
||||
import api from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
import { initials } from "@/mixins/initials";
|
||||
export default {
|
||||
pageTitle: "My Profile",
|
||||
components: {
|
||||
UploadBtn,
|
||||
},
|
||||
mixins: [validators, initials],
|
||||
data() {
|
||||
return {
|
||||
hideImage: false,
|
||||
passwordLoading: false,
|
||||
password: {
|
||||
current: "",
|
||||
newOne: "",
|
||||
newTwo: "",
|
||||
},
|
||||
showPassword: false,
|
||||
loading: false,
|
||||
user: {
|
||||
fullName: "Change Me",
|
||||
|
@ -62,10 +171,15 @@ export default {
|
|||
admin: true,
|
||||
id: 1,
|
||||
},
|
||||
showAvatarPicker: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
userProfileImage() {
|
||||
return `api/users/${this.user.id}/image`;
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.refreshProfile();
|
||||
},
|
||||
|
@ -87,6 +201,21 @@ export default {
|
|||
this.refreshProfile();
|
||||
this.loading = false;
|
||||
},
|
||||
async changePassword() {
|
||||
this.paswordLoading = true;
|
||||
let data = {
|
||||
currentPassword: this.password.current,
|
||||
newPassword: this.password.newOne,
|
||||
};
|
||||
|
||||
if (this.$refs.passChange.validate()) {
|
||||
await api.users.changePassword(this.user.id, data);
|
||||
}
|
||||
this.paswordLoading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,18 +1,73 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-container>
|
||||
<AdminSidebar />
|
||||
<v-slide-x-transition hide-on-leave>
|
||||
<router-view></router-view>
|
||||
</v-slide-x-transition>
|
||||
</div>
|
||||
<!-- <v-footer fixed>
|
||||
<v-col class="text-center" cols="12">
|
||||
{{ $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>
|
||||
</v-col>
|
||||
</v-footer> -->
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AdminSidebar from "../../components/Admin/AdminSidebar";
|
||||
import AdminSidebar from "@/components/Admin/AdminSidebar";
|
||||
import axios from "axios";
|
||||
import api from "@/api";
|
||||
export default {
|
||||
components: {
|
||||
AdminSidebar,
|
||||
},
|
||||
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",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: null,
|
||||
},
|
||||
}
|
||||
);
|
||||
this.latestVersion = response.data.tag_name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-container>
|
||||
<CategorySidebar />
|
||||
<CardSection
|
||||
:sortable="true"
|
||||
|
@ -9,7 +9,7 @@
|
|||
@sort="sortAZ"
|
||||
@sort-recent="sortRecent"
|
||||
/>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-container>
|
||||
<CategorySidebar />
|
||||
<CardSection
|
||||
:sortable="true"
|
||||
|
@ -9,7 +9,7 @@
|
|||
@sort="sortAZ"
|
||||
@sort-recent="sortRecent"
|
||||
/>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-container>
|
||||
<LastRecipe />
|
||||
<LogFile class="mt-2" />
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-container>
|
||||
<CategorySidebar />
|
||||
<CardSection
|
||||
v-if="showRecent"
|
||||
|
@ -17,7 +17,7 @@
|
|||
@sort="sortAZ(index)"
|
||||
@sort-recent="sortRecent(index)"
|
||||
/>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
<template>
|
||||
<v-row justify="start" height="100%">
|
||||
<v-col align="center">
|
||||
<LoginForm />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-container fill-height class="text-center">
|
||||
<v-row>
|
||||
<v-flex class="d-flex justify-center" width="500px">
|
||||
<LoginForm @logged-in="redirectMe" class="ma-1" />
|
||||
</v-flex>
|
||||
</v-row>
|
||||
<v-row></v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -12,6 +15,25 @@ export default {
|
|||
components: {
|
||||
LoginForm,
|
||||
},
|
||||
computed: {
|
||||
viewScale() {
|
||||
switch (this.$vuetify.breakpoint.name) {
|
||||
case "xs":
|
||||
return true;
|
||||
case "sm":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
redirectMe() {
|
||||
if (this.$route.query.redirect) {
|
||||
this.$router.push(this.$route.query.redirect);
|
||||
} else this.$router.push({ path: "/" });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-container>
|
||||
<EditPlan
|
||||
v-if="editMealPlan"
|
||||
:meal-plan="editMealPlan"
|
||||
|
@ -25,8 +25,8 @@
|
|||
>
|
||||
<v-card class="mt-1">
|
||||
<v-card-title>
|
||||
{{ $d( new Date(mealplan.startDate), 'short' ) }} -
|
||||
{{ $d( new Date(mealplan.endDate), 'short' ) }}
|
||||
{{ $d(new Date(mealplan.startDate), "short") }} -
|
||||
{{ $d(new Date(mealplan.endDate), "short") }}
|
||||
</v-card-title>
|
||||
<v-list nav>
|
||||
<v-list-item-group color="primary">
|
||||
|
@ -43,7 +43,9 @@
|
|||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="meal.name"></v-list-item-title>
|
||||
<v-list-item-subtitle v-text="$d( new Date(meal.date), 'short' )" >
|
||||
<v-list-item-subtitle
|
||||
v-text="$d(new Date(meal.date), 'short')"
|
||||
>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
@ -79,7 +81,7 @@
|
|||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
</v-card>
|
||||
</v-col>
|
||||
<v-col order-sm="0" :order-md="getOrder(index)" md="6" sm="12">
|
||||
<v-card>
|
||||
<v-card flat>
|
||||
<v-img :src="getImage(meal.image)" max-height="300"> </v-img>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
|
|
@ -1,41 +1,43 @@
|
|||
<template>
|
||||
<v-card :loading="isLoading">
|
||||
<v-img v-if="image" height="400" :src="image">
|
||||
<template v-slot:placeholder>
|
||||
<v-row class="fill-height ma-0" align="center" justify="center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="grey lighten-5"
|
||||
></v-progress-circular>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-img>
|
||||
<br v-else />
|
||||
<v-container>
|
||||
<v-card :loading="isLoading">
|
||||
<v-img v-if="image" height="400" :src="image">
|
||||
<template v-slot:placeholder>
|
||||
<v-row class="fill-height ma-0" align="center" justify="center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="grey lighten-5"
|
||||
></v-progress-circular>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-img>
|
||||
<br v-else />
|
||||
|
||||
<EditorButtonRow
|
||||
@json="jsonEditor = true"
|
||||
@editor="jsonEditor = false"
|
||||
@save="createRecipe"
|
||||
/>
|
||||
|
||||
<div v-if="jsonEditor">
|
||||
<!-- Probably not the best way, but it works! -->
|
||||
<br />
|
||||
<br />
|
||||
<VJsoneditor
|
||||
v-model="recipeDetails"
|
||||
height="1500px"
|
||||
:options="jsonEditorOptions"
|
||||
<EditorButtonRow
|
||||
@json="jsonEditor = true"
|
||||
@editor="jsonEditor = false"
|
||||
@save="createRecipe"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RecipeEditor
|
||||
ref="recipeEditor"
|
||||
v-else
|
||||
v-model="recipeDetails"
|
||||
@upload="getImage"
|
||||
/>
|
||||
</v-card>
|
||||
<div v-if="jsonEditor">
|
||||
<!-- Probably not the best way, but it works! -->
|
||||
<br />
|
||||
<br />
|
||||
<VJsoneditor
|
||||
v-model="recipeDetails"
|
||||
height="1500px"
|
||||
:options="jsonEditorOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RecipeEditor
|
||||
ref="recipeEditor"
|
||||
v-else
|
||||
v-model="recipeDetails"
|
||||
@upload="getImage"
|
||||
/>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,58 +1,61 @@
|
|||
<template>
|
||||
<v-card id="myRecipe">
|
||||
<v-img
|
||||
height="400"
|
||||
:src="getImage(recipeDetails.image)"
|
||||
class="d-print-none"
|
||||
:key="imageKey"
|
||||
>
|
||||
<RecipeTimeCard
|
||||
class="force-bottom"
|
||||
:prepTime="recipeDetails.prepTime"
|
||||
:totalTime="recipeDetails.totalTime"
|
||||
:performTime="recipeDetails.performTime"
|
||||
<v-container>
|
||||
<v-card id="myRecipe">
|
||||
<v-img
|
||||
height="400"
|
||||
:src="getImage(recipeDetails.image)"
|
||||
class="d-print-none"
|
||||
:key="imageKey"
|
||||
>
|
||||
<RecipeTimeCard
|
||||
class="force-bottom"
|
||||
:prepTime="recipeDetails.prepTime"
|
||||
:totalTime="recipeDetails.totalTime"
|
||||
:performTime="recipeDetails.performTime"
|
||||
/>
|
||||
</v-img>
|
||||
<EditorButtonRow
|
||||
v-if="loggedIn"
|
||||
:open="showIcons"
|
||||
@json="jsonEditor = true"
|
||||
@editor="
|
||||
jsonEditor = false;
|
||||
form = true;
|
||||
"
|
||||
@save="saveRecipe"
|
||||
@delete="deleteRecipe"
|
||||
class="sticky"
|
||||
/>
|
||||
</v-img>
|
||||
<EditorButtonRow
|
||||
:open="showIcons"
|
||||
@json="jsonEditor = true"
|
||||
@editor="
|
||||
jsonEditor = false;
|
||||
form = true;
|
||||
"
|
||||
@save="saveRecipe"
|
||||
@delete="deleteRecipe"
|
||||
class="sticky"
|
||||
/>
|
||||
|
||||
<RecipeViewer
|
||||
v-if="!form"
|
||||
:name="recipeDetails.name"
|
||||
:ingredients="recipeDetails.recipeIngredient"
|
||||
:description="recipeDetails.description"
|
||||
:instructions="recipeDetails.recipeInstructions"
|
||||
:tags="recipeDetails.tags"
|
||||
:categories="recipeDetails.categories"
|
||||
:notes="recipeDetails.notes"
|
||||
:rating="recipeDetails.rating"
|
||||
:yields="recipeDetails.recipeYield"
|
||||
:orgURL="recipeDetails.orgURL"
|
||||
/>
|
||||
<VJsoneditor
|
||||
@error="logError()"
|
||||
class="mt-10"
|
||||
v-else-if="showJsonEditor"
|
||||
v-model="recipeDetails"
|
||||
height="1500px"
|
||||
:options="jsonEditorOptions"
|
||||
/>
|
||||
<RecipeEditor
|
||||
v-else
|
||||
v-model="recipeDetails"
|
||||
ref="recipeEditor"
|
||||
@upload="getImageFile"
|
||||
/>
|
||||
</v-card>
|
||||
<RecipeViewer
|
||||
v-if="!form"
|
||||
:name="recipeDetails.name"
|
||||
:ingredients="recipeDetails.recipeIngredient"
|
||||
:description="recipeDetails.description"
|
||||
:instructions="recipeDetails.recipeInstructions"
|
||||
:tags="recipeDetails.tags"
|
||||
:categories="recipeDetails.categories"
|
||||
:notes="recipeDetails.notes"
|
||||
:rating="recipeDetails.rating"
|
||||
:yields="recipeDetails.recipeYield"
|
||||
:orgURL="recipeDetails.orgURL"
|
||||
/>
|
||||
<VJsoneditor
|
||||
@error="logError()"
|
||||
class="mt-10"
|
||||
v-else-if="showJsonEditor"
|
||||
v-model="recipeDetails"
|
||||
height="1500px"
|
||||
:options="jsonEditorOptions"
|
||||
/>
|
||||
<RecipeEditor
|
||||
v-else
|
||||
v-model="recipeDetails"
|
||||
ref="recipeEditor"
|
||||
@upload="getImageFile"
|
||||
/>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -63,6 +66,7 @@ import RecipeViewer from "../components/Recipe/RecipeViewer";
|
|||
import RecipeEditor from "../components/Recipe/RecipeEditor";
|
||||
import RecipeTimeCard from "../components/Recipe/RecipeTimeCard.vue";
|
||||
import EditorButtonRow from "../components/Recipe/EditorButtonRow";
|
||||
import { user } from "@/mixins/user";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -72,6 +76,7 @@ export default {
|
|||
EditorButtonRow,
|
||||
RecipeTimeCard,
|
||||
},
|
||||
mixins: [user],
|
||||
data() {
|
||||
return {
|
||||
// currentRecipe: this.$route.params.recipe,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-container>
|
||||
<v-row justify="center">
|
||||
<v-col cols="1"> </v-col>
|
||||
<v-col>
|
||||
|
@ -30,7 +30,7 @@
|
|||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -6,10 +6,16 @@ import Migration from "@/pages/Admin/Migration";
|
|||
import Profile from "@/pages/Admin/Profile";
|
||||
import ManageUsers from "@/pages/Admin/ManageUsers";
|
||||
import Settings from "@/pages/Admin/Settings";
|
||||
import { store } from "../store";
|
||||
|
||||
export default {
|
||||
path: "/admin",
|
||||
component: Admin,
|
||||
beforeEnter: (to, _from, next) => {
|
||||
if (store.getters.getIsLoggedIn) {
|
||||
next();
|
||||
} else next({ path: "/login", query: { redirect: to.fullPath } });
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
|
|
|
@ -11,7 +11,7 @@ import LoginPage from "../pages/LoginPage";
|
|||
import MealPlanThisWeekPage from "../pages/MealPlanThisWeekPage";
|
||||
import api from "@/api";
|
||||
import Admin from "./admin";
|
||||
import { store } from "../store/store";
|
||||
import { store } from "../store";
|
||||
|
||||
export const routes = [
|
||||
{ path: "/", name: "home", component: HomePage },
|
||||
|
|
|
@ -21,6 +21,7 @@ const state = {
|
|||
isDark: false,
|
||||
isLoggedIn: false,
|
||||
token: "",
|
||||
userData: {},
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
|
@ -46,6 +47,10 @@ const mutations = {
|
|||
axios.defaults.headers.common["Authorization"] = `Bearer ${payload}`;
|
||||
state.token = payload;
|
||||
},
|
||||
|
||||
setUserData(state, payload) {
|
||||
state.userData = payload;
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
|
@ -77,6 +82,7 @@ const getters = {
|
|||
getIsDark: state => state.isDark,
|
||||
getIsLoggedIn: state => state.isLoggedIn,
|
||||
getToken: state => state.token,
|
||||
getUserData: state => state.userData,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
// import utils from "@/utils";
|
||||
// import Vue from "vue";
|
||||
// import Vuetify from "./plugins/vuetify";
|
||||
import { vueApp } from "./main";
|
||||
import { vueApp } from "../main";
|
||||
|
||||
|
||||
const notifyHelpers = {
|
||||
baseCSS: "notify-base",
|
|
@ -12,7 +12,7 @@ def ensure_dirs():
|
|||
|
||||
|
||||
# Register ENV
|
||||
ENV = CWD.joinpath(".env")
|
||||
ENV = CWD.joinpath(".env") #! I'm Broken Fix Me!
|
||||
dotenv.load_dotenv(ENV)
|
||||
|
||||
|
||||
|
@ -45,7 +45,9 @@ MIGRATION_DIR = DATA_DIR.joinpath("migration")
|
|||
NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud")
|
||||
CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown")
|
||||
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
|
||||
USER_DIR = DATA_DIR.joinpath("users")
|
||||
SQLITE_DIR = DATA_DIR.joinpath("db")
|
||||
RECIPE_DATA_DIR = DATA_DIR.joinpath("recipes")
|
||||
TEMP_DIR = DATA_DIR.joinpath(".temp")
|
||||
|
||||
REQUIRED_DIRS = [
|
||||
|
@ -58,6 +60,8 @@ REQUIRED_DIRS = [
|
|||
SQLITE_DIR,
|
||||
NEXTCLOUD_DIR,
|
||||
CHOWDOWN_DIR,
|
||||
RECIPE_DATA_DIR,
|
||||
USER_DIR,
|
||||
]
|
||||
|
||||
ensure_dirs()
|
||||
|
|
|
@ -61,6 +61,15 @@ class _Users(BaseDocument):
|
|||
self.primary_key = "id"
|
||||
self.sql_model = User
|
||||
|
||||
def update_password(self, session, id, password: str):
|
||||
entry = self._query_one(session=session, match_value=id)
|
||||
entry.update_password(password)
|
||||
return_data = entry.dict()
|
||||
session.commit()
|
||||
|
||||
return return_data
|
||||
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self) -> None:
|
||||
|
|
|
@ -42,3 +42,6 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||
self.email = email
|
||||
self.family = family
|
||||
self.admin = admin
|
||||
|
||||
def update_password(self, password):
|
||||
self.password = password
|
||||
|
|
|
@ -79,7 +79,7 @@ def filter_by_category(categories: list, session: Session = Depends(generate_ses
|
|||
in_category = [
|
||||
db.categories.get(session, slugify(cat), limit=1) for cat in categories
|
||||
]
|
||||
in_category = [cat.get("recipes") for cat in in_category]
|
||||
in_category = [cat.get("recipes") for cat in in_category if cat]
|
||||
in_category = [item for sublist in in_category for item in sublist]
|
||||
return in_category
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ 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.snackbar import SnackResponse
|
||||
from schema.user import UserInDB
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
|
@ -13,7 +14,7 @@ router = APIRouter(prefix="/api/auth", tags=["Auth"])
|
|||
|
||||
|
||||
@router.post("/token")
|
||||
def token(
|
||||
def get_token(
|
||||
data: OAuth2PasswordRequestForm = Depends(),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
@ -29,4 +30,42 @@ def token(
|
|||
access_token = manager.create_access_token(
|
||||
data=dict(sub=email), expires=timedelta(hours=2)
|
||||
)
|
||||
return SnackResponse.success(
|
||||
"User Successfully Logged In",
|
||||
{"access_token": access_token, "token_type": "bearer"},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/token/long")
|
||||
def get_long_token(
|
||||
data: OAuth2PasswordRequestForm = Depends(),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
"""Get an Access Token for 1 day"""
|
||||
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(days=1)
|
||||
)
|
||||
return SnackResponse.success(
|
||||
"User Successfully Logged In",
|
||||
{"access_token": access_token, "token_type": "bearer"},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh")
|
||||
async def refresh_token(
|
||||
current_user: UserInDB = Depends(manager),
|
||||
):
|
||||
""" Use a valid token to get another token"""
|
||||
access_token = manager.create_access_token(
|
||||
data=dict(sub=current_user.email), expires=timedelta(hours=1)
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import shutil
|
||||
from datetime import timedelta
|
||||
|
||||
from core.security import get_password_hash
|
||||
from core.config import USER_DIR
|
||||
from core.security import get_password_hash, verify_password
|
||||
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 fastapi import APIRouter, Depends, File, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from routes.deps import manager
|
||||
from schema.snackbar import SnackResponse
|
||||
from schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["Users"])
|
||||
|
@ -17,12 +21,11 @@ async def create_user(
|
|||
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
|
||||
return SnackResponse.success(f"User Created: {new_user.full_name}", data)
|
||||
|
||||
|
||||
@router.get("", response_model=list[UserOut])
|
||||
|
@ -38,7 +41,7 @@ async def get_all_users(
|
|||
|
||||
|
||||
@router.get("/self", response_model=UserOut)
|
||||
async def get_user_by_id(
|
||||
async def get_logged_in_user(
|
||||
current_user: UserInDB = Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
@ -69,8 +72,71 @@ async def update_user(
|
|||
access_token = manager.create_access_token(
|
||||
data=dict(sub=email), expires=timedelta(hours=2)
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
return
|
||||
access_token = {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
return SnackResponse.success("User Updated", access_token)
|
||||
|
||||
|
||||
@router.get("/{id}/image")
|
||||
async def get_user_image(id: str):
|
||||
""" Returns a users profile picture """
|
||||
user_dir = USER_DIR.joinpath(id)
|
||||
for recipe_image in user_dir.glob("profile_image.*"):
|
||||
print(recipe_image)
|
||||
return FileResponse(recipe_image)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@router.post("/{id}/image")
|
||||
async def update_user_image(
|
||||
id: str,
|
||||
profile_image: UploadFile = File(...),
|
||||
current_user: UserInDB = Depends(manager),
|
||||
):
|
||||
""" Updates a User Image """
|
||||
|
||||
extension = profile_image.filename.split(".")[-1]
|
||||
|
||||
USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
[x.unlink() for x in USER_DIR.join(id).glob("profile_image.*")]
|
||||
except:
|
||||
pass
|
||||
|
||||
dest = USER_DIR.joinpath(id, f"profile_image.{extension}")
|
||||
|
||||
with dest.open("wb") as buffer:
|
||||
shutil.copyfileobj(profile_image.file, buffer)
|
||||
|
||||
if dest.is_file:
|
||||
return SnackResponse.success("Backup uploaded")
|
||||
else:
|
||||
return SnackResponse.error("Failure uploading file")
|
||||
|
||||
|
||||
@router.put("/{id}/password")
|
||||
async def update_password(
|
||||
id: int,
|
||||
password_change: ChangePassword,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Resets the User Password"""
|
||||
|
||||
match_passwords = verify_password(
|
||||
password_change.current_password, current_user.password
|
||||
)
|
||||
print(match_passwords)
|
||||
match_id = current_user.id == id
|
||||
|
||||
if match_passwords and match_id:
|
||||
new_password = get_password_hash(password_change.new_password)
|
||||
db.users.update_password(session, id, new_password)
|
||||
return SnackResponse.success("Password Updated")
|
||||
else:
|
||||
return SnackResponse.error("Existing password does not match")
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
|
@ -81,5 +147,9 @@ async def delete_user(
|
|||
):
|
||||
""" Removes a user from the database. Must be the current user or a super user"""
|
||||
|
||||
if id == 1:
|
||||
return SnackResponse.error("Error! Cannot Delete Super User")
|
||||
|
||||
if current_user.id == id or current_user.admin:
|
||||
return db.users.delete(session, id)
|
||||
db.users.delete(session, id)
|
||||
return SnackResponse.error(f"User Deleted")
|
||||
|
|
|
@ -5,6 +5,11 @@ from fastapi_camelcase import CamelModel
|
|||
# from pydantic import EmailStr
|
||||
|
||||
|
||||
class ChangePassword(CamelModel):
|
||||
current_password: str
|
||||
new_password: str
|
||||
|
||||
|
||||
class UserBase(CamelModel):
|
||||
full_name: Optional[str] = None
|
||||
email: str
|
||||
|
|
|
@ -6,16 +6,17 @@ from pathlib import Path
|
|||
from core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
|
||||
from db.database import db
|
||||
from db.db_setup import create_session
|
||||
from fastapi.logger import logger
|
||||
from jinja2 import Template
|
||||
from services.meal_services import MealPlan
|
||||
from services.recipe_services import Recipe
|
||||
from fastapi.logger import logger
|
||||
|
||||
|
||||
class ExportDatabase:
|
||||
def __init__(self, session, tag=None, templates=None) -> None:
|
||||
"""Export a Mealie database. Export interacts directly with class objects and can be used
|
||||
with any supported backend database platform. By default tags are timestands, and no Jinja2 templates are rendered
|
||||
with any supported backend database platform. By default tags are timestands, and no
|
||||
Jinja2 templates are rendered
|
||||
|
||||
|
||||
Args:
|
||||
|
|
249
poetry.lock
generated
249
poetry.lock
generated
|
@ -263,6 +263,14 @@ fastapi = "*"
|
|||
passlib = "*"
|
||||
pyjwt = "*"
|
||||
|
||||
[[package]]
|
||||
name = "future"
|
||||
version = "0.18.2"
|
||||
description = "Clean single-source support for Python 3 and 2"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.12.0"
|
||||
|
@ -365,6 +373,14 @@ MarkupSafe = ">=0.23"
|
|||
[package.extras]
|
||||
i18n = ["Babel (>=0.8)"]
|
||||
|
||||
[[package]]
|
||||
name = "joblib"
|
||||
version = "1.0.1"
|
||||
description = "Lightweight pipelining with Python functions"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "jstyleson"
|
||||
version = "0.0.2"
|
||||
|
@ -381,6 +397,34 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "livereload"
|
||||
version = "2.6.3"
|
||||
description = "Python LiveReload is an awesome tool for web developers"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
six = "*"
|
||||
tornado = {version = "*", markers = "python_version > \"2.7\""}
|
||||
|
||||
[[package]]
|
||||
name = "lunr"
|
||||
version = "0.5.8"
|
||||
description = "A Python implementation of Lunr.js"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
future = ">=0.16.0"
|
||||
nltk = {version = ">=3.2.5", optional = true, markers = "python_version > \"2.7\" and extra == \"languages\""}
|
||||
six = ">=1.11.0"
|
||||
|
||||
[package.extras]
|
||||
languages = ["nltk (>=3.2.5,<3.5)", "nltk (>=3.2.5)"]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "4.6.2"
|
||||
|
@ -395,6 +439,17 @@ html5 = ["html5lib"]
|
|||
htmlsoup = ["beautifulsoup4"]
|
||||
source = ["Cython (>=0.29.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "3.3.4"
|
||||
description = "Python implementation of Markdown."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
testing = ["coverage", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "1.1.1"
|
||||
|
@ -424,6 +479,49 @@ BeautifulSoup4 = ">=4.6.0"
|
|||
html5lib = ">=1.0.1"
|
||||
requests = ">=2.18.4"
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs"
|
||||
version = "1.1.2"
|
||||
description = "Project documentation with Markdown."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=3.3"
|
||||
Jinja2 = ">=2.10.1"
|
||||
livereload = ">=2.5.1"
|
||||
lunr = {version = "0.5.8", extras = ["languages"]}
|
||||
Markdown = ">=3.2.1"
|
||||
PyYAML = ">=3.10"
|
||||
tornado = ">=5.0"
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "7.0.2"
|
||||
description = "A Material Design theme for MkDocs"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
markdown = ">=3.2"
|
||||
mkdocs = ">=1.1"
|
||||
mkdocs-material-extensions = ">=1.0"
|
||||
Pygments = ">=2.4"
|
||||
pymdown-extensions = ">=7.0"
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material-extensions"
|
||||
version = "1.0.1"
|
||||
description = "Extension pack for Python Markdown."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.dependencies]
|
||||
mkdocs-material = ">=5.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "0.4.3"
|
||||
|
@ -432,6 +530,28 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "nltk"
|
||||
version = "3.5"
|
||||
description = "Natural Language Toolkit"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
click = "*"
|
||||
joblib = "*"
|
||||
regex = "*"
|
||||
tqdm = "*"
|
||||
|
||||
[package.extras]
|
||||
all = ["requests", "numpy", "python-crfsuite", "scikit-learn", "twython", "pyparsing", "scipy", "matplotlib", "gensim"]
|
||||
corenlp = ["requests"]
|
||||
machine_learning = ["gensim", "numpy", "python-crfsuite", "scikit-learn", "scipy"]
|
||||
plot = ["matplotlib"]
|
||||
tgrep = ["pyparsing"]
|
||||
twitter = ["twython"]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "20.8"
|
||||
|
@ -505,6 +625,14 @@ dotenv = ["python-dotenv (>=0.10.4)"]
|
|||
email = ["email-validator (>=1.0.3)"]
|
||||
typing_extensions = ["typing-extensions (>=3.7.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.8.0"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "pyhumps"
|
||||
version = "1.6.1"
|
||||
|
@ -542,6 +670,17 @@ isort = ">=4.2.5,<6"
|
|||
mccabe = ">=0.6,<0.7"
|
||||
toml = ">=0.7.1"
|
||||
|
||||
[[package]]
|
||||
name = "pymdown-extensions"
|
||||
version = "8.1.1"
|
||||
description = "Extension pack for Python Markdown."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
Markdown = ">=3.2"
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "2.4.7"
|
||||
|
@ -783,6 +922,26 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.1"
|
||||
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">= 3.5"
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.58.0"
|
||||
description = "Fast, Extensible Progress Meter"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
|
||||
|
||||
[package.extras]
|
||||
dev = ["py-make (>=0.1.0)", "twine", "wheel"]
|
||||
telegram = ["requests"]
|
||||
|
||||
[[package]]
|
||||
name = "typed-ast"
|
||||
version = "1.4.2"
|
||||
|
@ -914,7 +1073,7 @@ python-versions = "*"
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "182496243ce59b60506b646a0a521d0186a7ef36009c5212276807e13e20d8a3"
|
||||
content-hash = "adf8ad8e07d1af5c231c936276b57be83dd534e0cf706042ddb26f6ff51c86ca"
|
||||
|
||||
[metadata.files]
|
||||
aiofiles = [
|
||||
|
@ -1087,6 +1246,9 @@ fastapi-login = [
|
|||
{file = "fastapi-login-1.5.3.tar.gz", hash = "sha256:8e8ef710f1b7107e81d00e205779e73e17be35d5a91d11685ff72f323898e93b"},
|
||||
{file = "fastapi_login-1.5.3-py3-none-any.whl", hash = "sha256:6c83b74bdb45c34ec0aab22000a7951df96c5d011f02a99a46ca4b2be6b1263c"},
|
||||
]
|
||||
future = [
|
||||
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
|
||||
]
|
||||
h11 = [
|
||||
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
|
||||
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
|
||||
|
@ -1133,6 +1295,10 @@ jinja2 = [
|
|||
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
|
||||
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
||||
]
|
||||
joblib = [
|
||||
{file = "joblib-1.0.1-py3-none-any.whl", hash = "sha256:feeb1ec69c4d45129954f1b7034954241eedfd6ba39b5e9e4b6883be3332d5e5"},
|
||||
{file = "joblib-1.0.1.tar.gz", hash = "sha256:9c17567692206d2f3fb9ecf5e991084254fe631665c450b443761c4186a613f7"},
|
||||
]
|
||||
jstyleson = [
|
||||
{file = "jstyleson-0.0.2.tar.gz", hash = "sha256:680003f3b15a2959e4e6a351f3b858e3c07dd3e073a0d54954e34d8ea5e1308e"},
|
||||
]
|
||||
|
@ -1159,6 +1325,13 @@ lazy-object-proxy = [
|
|||
{file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"},
|
||||
{file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"},
|
||||
]
|
||||
livereload = [
|
||||
{file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"},
|
||||
]
|
||||
lunr = [
|
||||
{file = "lunr-0.5.8-py2.py3-none-any.whl", hash = "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca"},
|
||||
{file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"},
|
||||
]
|
||||
lxml = [
|
||||
{file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a9d6bc8642e2c67db33f1247a77c53476f3a166e09067c0474facb045756087f"},
|
||||
{file = "lxml-4.6.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:791394449e98243839fa822a637177dd42a95f4883ad3dec2a0ce6ac99fb0a9d"},
|
||||
|
@ -1198,6 +1371,10 @@ lxml = [
|
|||
{file = "lxml-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:535332fe9d00c3cd455bd3dd7d4bacab86e2d564bdf7606079160fa6251caacf"},
|
||||
{file = "lxml-4.6.2.tar.gz", hash = "sha256:cd11c7e8d21af997ee8079037fff88f16fda188a9776eb4b81c7e4c9c0a7d7fc"},
|
||||
]
|
||||
markdown = [
|
||||
{file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"},
|
||||
{file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"},
|
||||
]
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
|
||||
|
@ -1259,10 +1436,25 @@ mccabe = [
|
|||
mf2py = [
|
||||
{file = "mf2py-1.1.2.tar.gz", hash = "sha256:84f1f8f2ff3f1deb1c30be497e7ccd805452996a662fd4a77f09e0105bede2c9"},
|
||||
]
|
||||
mkdocs = [
|
||||
{file = "mkdocs-1.1.2-py3-none-any.whl", hash = "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9"},
|
||||
{file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"},
|
||||
]
|
||||
mkdocs-material = [
|
||||
{file = "mkdocs-material-7.0.2.tar.gz", hash = "sha256:853b9276a32700fed7fbdf608d6ac39297edcdf8cb683c6337acb412d9c7bb57"},
|
||||
{file = "mkdocs_material-7.0.2-py2.py3-none-any.whl", hash = "sha256:cb2ba082f91e6ca8517589cddf31f9f9134cd081aa50c7973895734c8d56f13b"},
|
||||
]
|
||||
mkdocs-material-extensions = [
|
||||
{file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"},
|
||||
{file = "mkdocs_material_extensions-1.0.1-py3-none-any.whl", hash = "sha256:d90c807a88348aa6d1805657ec5c0b2d8d609c110e62b9dce4daf7fa981fa338"},
|
||||
]
|
||||
mypy-extensions = [
|
||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||
]
|
||||
nltk = [
|
||||
{file = "nltk-3.5.zip", hash = "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"},
|
||||
{file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"},
|
||||
|
@ -1311,6 +1503,10 @@ pydantic = [
|
|||
{file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"},
|
||||
{file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"},
|
||||
]
|
||||
pygments = [
|
||||
{file = "Pygments-2.8.0-py3-none-any.whl", hash = "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88"},
|
||||
{file = "Pygments-2.8.0.tar.gz", hash = "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0"},
|
||||
]
|
||||
pyhumps = [
|
||||
{file = "pyhumps-1.6.1-py3-none-any.whl", hash = "sha256:58b367b73c57b64e32d211dc769addabd68ff6db07ce64b2e6565f7d5a12291f"},
|
||||
{file = "pyhumps-1.6.1.tar.gz", hash = "sha256:01612603c5ad73a407299d806d30708a3935052276fdd93776953bccc0724e0a"},
|
||||
|
@ -1323,6 +1519,10 @@ pylint = [
|
|||
{file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"},
|
||||
{file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"},
|
||||
]
|
||||
pymdown-extensions = [
|
||||
{file = "pymdown-extensions-8.1.1.tar.gz", hash = "sha256:632371fa3bf1b21a0e3f4063010da59b41db049f261f4c0b0872069a9b6d1735"},
|
||||
{file = "pymdown_extensions-8.1.1-py3-none-any.whl", hash = "sha256:478b2c04513fbb2db61688d5f6e9030a92fb9be14f1f383535c43f7be9dff95b"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
|
||||
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
|
||||
|
@ -1494,6 +1694,53 @@ toml = [
|
|||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||
]
|
||||
tornado = [
|
||||
{file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"},
|
||||
{file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"},
|
||||
{file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"},
|
||||
{file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"},
|
||||
{file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"},
|
||||
{file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"},
|
||||
{file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"},
|
||||
{file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"},
|
||||
{file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"},
|
||||
{file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"},
|
||||
{file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"},
|
||||
{file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"},
|
||||
{file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"},
|
||||
{file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"},
|
||||
{file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"},
|
||||
{file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"},
|
||||
{file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"},
|
||||
{file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"},
|
||||
{file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"},
|
||||
{file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"},
|
||||
{file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"},
|
||||
{file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"},
|
||||
{file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"},
|
||||
{file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"},
|
||||
{file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"},
|
||||
{file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"},
|
||||
{file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"},
|
||||
{file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"},
|
||||
{file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"},
|
||||
{file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"},
|
||||
{file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"},
|
||||
{file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"},
|
||||
{file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"},
|
||||
{file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"},
|
||||
{file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"},
|
||||
{file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"},
|
||||
{file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"},
|
||||
{file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"},
|
||||
{file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"},
|
||||
{file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"},
|
||||
{file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"},
|
||||
]
|
||||
tqdm = [
|
||||
{file = "tqdm-4.58.0-py2.py3-none-any.whl", hash = "sha256:2c44efa73b8914dba7807aefd09653ac63c22b5b4ea34f7a80973f418f1a3089"},
|
||||
{file = "tqdm-4.58.0.tar.gz", hash = "sha256:c23ac707e8e8aabb825e4d91f8e17247f9cc14b0d64dd9e97be0781e9e525bba"},
|
||||
]
|
||||
typed-ast = [
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
|
||||
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
|
||||
|
|
|
@ -26,14 +26,15 @@ extruct = "^0.12.0"
|
|||
scrape-schema-recipe = "^0.1.3"
|
||||
python-multipart = "^0.0.5"
|
||||
fastapi-login = "^1.5.3"
|
||||
bcrypt = "^3.2.0"
|
||||
fastapi-camelcase = "^1.0.2"
|
||||
bcrypt = "^3.2.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pylint = "^2.6.0"
|
||||
black = "^20.8b1"
|
||||
pytest = "^6.2.1"
|
||||
pytest-cov = "^2.11.0"
|
||||
mkdocs-material = "^7.0.2"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue