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:
Hayden 2021-02-25 20:41:34 -09:00 committed by GitHub
commit 9af664c259
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 1365 additions and 462 deletions

1
.gitignore vendored
View file

@ -14,6 +14,7 @@ app_data/backups/*
app_data/debug/* app_data/debug/*
app_data/img/* app_data/img/*
app_data/migration/* app_data/migration/*
app_data/users/*
#Exception to keep folders #Exception to keep folders
!mealie/dist/.gitkeep !mealie/dist/.gitkeep

View file

@ -1,6 +1,6 @@
{ {
"python.formatting.provider": "black", "python.formatting.provider": "black",
"python.pythonPath": ".venv/bin/python3.9", "python.pythonPath": "./.venv/bin/python3.9",
"python.linting.pylintEnabled": true, "python.linting.pylintEnabled": true,
"python.linting.enabled": true, "python.linting.enabled": true,
"python.autoComplete.extraPaths": ["mealie", "mealie/mealie"], "python.autoComplete.extraPaths": ["mealie", "mealie/mealie"],

View file

@ -1,6 +1,6 @@
# Site Settings Panel # Site Settings Panel
!!! danger !!! 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 ## 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. 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
![](../gifs/theme-demo-v2.gif) ![](../gifs/theme-demo-v2.gif)
!!! 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. 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.

View 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

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View file

@ -29,6 +29,7 @@ nav:
- Getting Started: - Getting Started:
- Installation: "getting-started/install.md" - Installation: "getting-started/install.md"
- Working With Recipes: "getting-started/recipes.md" - Working With Recipes: "getting-started/recipes.md"
- User Management: "getting-started/users.md"
- Planning Meals: "getting-started/meal-planner.md" - Planning Meals: "getting-started/meal-planner.md"
- Site Settings: "getting-started/site-settings.md" - Site Settings: "getting-started/site-settings.md"
- Backups and Exports: "getting-started/backups-and-exports.md" - Backups and Exports: "getting-started/backups-and-exports.md"

View file

@ -23,16 +23,6 @@
"markdown-it-sup": "^1.0.0", "markdown-it-sup": "^1.0.0",
"markdown-it-task-lists": "^2.1.1", "markdown-it-task-lists": "^2.1.1",
"markdown-it-toc-and-anchor": "^4.2.0" "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": { "@babel/code-frame": {
@ -2016,6 +2006,16 @@
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true "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": { "cacache": {
"version": "13.0.1", "version": "13.0.1",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz",
@ -2042,6 +2042,53 @@
"unique-filename": "^1.1.1" "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": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -2058,6 +2105,16 @@
"minipass": "^3.1.1" "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": { "terser-webpack-plugin": {
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz",
@ -2074,6 +2131,18 @@
"terser": "^4.6.12", "terser": "^4.6.12",
"webpack-sources": "^1.4.3" "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", "resolved": "https://registry.npmjs.org/markdown-it-ins/-/markdown-it-ins-3.0.1.tgz",
"integrity": "sha512-32SSfZqSzqyAmmQ4SHvhxbFqSzPDqsZgMHDwxqPzp+v+t8RsmqsBZRG+RfRQskJko9PfKC2/oxyOs4Yg/CfiRw==" "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": { "markdown-it-mark": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/markdown-it-mark/-/markdown-it-mark-3.0.1.tgz", "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": { "vue-router": {
"version": "3.4.9", "version": "3.4.9",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.9.tgz",

View file

@ -31,10 +31,10 @@
<LanguageMenu /> <LanguageMenu />
</v-app-bar> </v-app-bar>
<v-main> <v-main>
<v-container> <v-slide-x-reverse-transition>
<AddRecipeFab /> <AddRecipeFab v-if="loggedIn" />
<router-view></router-view> </v-slide-x-reverse-transition>
</v-container> <router-view></router-view>
<FlashMessage :position="'right bottom'"></FlashMessage> <FlashMessage :position="'right bottom'"></FlashMessage>
</v-main> </v-main>
</v-app> </v-app>
@ -46,6 +46,7 @@ import SearchBar from "@/components/UI/Search/SearchBar";
import AddRecipeFab from "@/components/UI/AddRecipeFab"; import AddRecipeFab from "@/components/UI/AddRecipeFab";
import LanguageMenu from "@/components/UI/LanguageMenu"; import LanguageMenu from "@/components/UI/LanguageMenu";
import Vuetify from "./plugins/vuetify"; import Vuetify from "./plugins/vuetify";
import { user } from "@/mixins/user";
export default { export default {
name: "App", name: "App",
@ -57,6 +58,8 @@ export default {
LanguageMenu, LanguageMenu,
}, },
mixins: [user],
watch: { watch: {
$route() { $route() {
this.search = false; this.search = false;

View file

@ -1,7 +1,7 @@
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"; import { store } from "../store";
axios.defaults.headers.common[ axios.defaults.headers.common[
"Authorization" "Authorization"

View file

@ -1,6 +1,6 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import { store } from "../store/store"; import { store } from "../store";
const backupBase = baseURL + "backups/"; const backupBase = baseURL + "backups/";

View file

@ -5,8 +5,8 @@ const prefix = baseURL + "categories";
const categoryURLs = { const categoryURLs = {
get_all: `${prefix}`, get_all: `${prefix}`,
get_category: (category) => `${prefix}/${category}`, get_category: category => `${prefix}/${category}`,
delete_category: (category) => `${prefix}/${category}`, delete_category: category => `${prefix}/${category}`,
}; };
export default { export default {

View file

@ -1,6 +1,6 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import { store } from "../store/store"; import { store } from "../store";
const migrationBase = baseURL + "migrations"; const migrationBase = baseURL + "migrations";

View file

@ -1,6 +1,6 @@
import { baseURL } from "./api-utils"; import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils"; import { apiReq } from "./api-utils";
import { store } from "../store/store"; import { store } from "../store";
import { router } from "../main"; import { router } from "../main";
import qs from "qs"; import qs from "qs";

View file

@ -11,6 +11,7 @@ const usersURLs = {
users: `${userPrefix}`, users: `${userPrefix}`,
self: `${userPrefix}/self`, self: `${userPrefix}/self`,
userID: id => `${userPrefix}/${id}`, userID: id => `${userPrefix}/${id}`,
password: id => `${userPrefix}/${id}/password`,
}; };
export default { export default {
@ -42,6 +43,10 @@ export default {
let response = await apiReq.put(usersURLs.userID(user.id), user); let response = await apiReq.put(usersURLs.userID(user.id), user);
return response.data; return response.data;
}, },
async changePassword(id, password) {
let response = await apiReq.put(usersURLs.password(id), password);
return response.data;
},
async delete(id) { async delete(id) {
let response = await apiReq.delete(usersURLs.userID(id)); let response = await apiReq.delete(usersURLs.userID(id));
return response.data; return response.data;

View file

@ -22,13 +22,22 @@
> >
<template v-slot:prepend> <template v-slot:prepend>
<v-list-item two-line> <v-list-item two-line>
<v-list-item-avatar> <v-list-item-avatar color="accent" class="white--text">
<img src="https://randomuser.me/api/portraits/women/81.jpg" /> <img
:src="userProfileImage"
v-if="!hideImage"
@error="hideImage = true"
/>
<div v-else>
{{ initials }}
</div>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Jane Smith</v-list-item-title> <v-list-item-title> {{ user.fullName }}</v-list-item-title>
<v-list-item-subtitle>Admin</v-list-item-subtitle> <v-list-item-subtitle>
{{ user.admin ? "Admin" : "User" }}</v-list-item-subtitle
>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
</template> </template>
@ -50,7 +59,7 @@
</v-list> </v-list>
<v-divider></v-divider> <v-divider></v-divider>
<v-list nav dense> <v-list nav dense v-if="user.admin">
<v-list-item <v-list-item
v-for="nav in superLinks" v-for="nav in superLinks"
:key="nav.title" :key="nav.title"
@ -68,9 +77,14 @@
</template> </template>
<script> <script>
import { validators } from "@/mixins/validators";
import { initials } from "@/mixins/initials";
import { user } from "@/mixins/user";
export default { export default {
mixins: [validators, initials, user],
data() { data() {
return { return {
hideImage: false,
showSidebar: false, showSidebar: false,
mobile: false, mobile: false,
links: [], links: [],
@ -115,11 +129,17 @@ export default {
], ],
}; };
}, },
mounted() { async mounted() {
this.mobile = this.viewScale(); this.mobile = this.viewScale();
this.showSidebar = !this.viewScale(); this.showSidebar = !this.viewScale();
}, },
computed: {
userProfileImage() {
return `api/users/${this.user.id}/image`;
},
},
methods: { methods: {
viewScale() { viewScale() {
switch (this.$vuetify.breakpoint.name) { switch (this.$vuetify.breakpoint.name) {

View file

@ -1,10 +1,13 @@
<template> <template>
<v-card flat> <v-card flat>
<v-card-text> <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-row align="center" justify="center" dense class="mb-n7 pb-n5">
<v-col cols="12" sm="3" md="2"> <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>
<v-col cols="12" sm="5" md="5"> <v-col cols="12" sm="5" md="5">
<v-slider <v-slider

View file

@ -46,7 +46,7 @@
</v-card> </v-card>
</div> </div>
<div v-else> <div v-else>
<v-card class="text-center ma-2"> <v-card outlined class="text-center ma-2">
<v-card-text> <v-card-text>
{{ $t("migration.no-migration-data-available") }} {{ $t("migration.no-migration-data-available") }}
</v-card-text> </v-card-text>

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="text-center"> <div class="text-center">
<v-dialog v-model="dialog" width="500"> <v-dialog v-model="dialog" width="500px">
<LoginForm @logged-in="dialog = false" /> <LoginForm @logged-in="dialog = false" />
</v-dialog> </v-dialog>
</div> </div>

View file

@ -1,93 +1,65 @@
<template> <template>
<div> <v-card width="500px">
<v-card max-width="500px"> <v-divider></v-divider>
<v-divider></v-divider> <v-app-bar dark color="primary" class="mt-n1 mb-4">
<v-app-bar dark color="primary mt-n1"> <v-icon large left v-if="!loading">
<v-icon large left v-if="!loading"> mdi-account
mdi-account </v-icon>
</v-icon> <v-progress-circular
<v-progress-circular v-else
v-else indeterminate
indeterminate color="white"
color="white" large
large class="mr-2"
class="mr-2" >
> </v-progress-circular>
</v-progress-circular> <v-toolbar-title class="headline"> Login </v-toolbar-title>
<v-toolbar-title class="headline"> Login </v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
</v-app-bar> </v-app-bar>
<v-card-text> <v-card-text>
<v-form> <v-form>
<v-text-field <v-text-field
v-if="!options.isLoggingIn" v-if="!options.isLoggingIn"
v-model="user.name" 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" 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-form>
</v-btn> <v-alert v-if="error" outlined class="mt-3 mb-0" type="error">
</v-card-actions> --> Could Not Validate Credentials
</v-card> </v-alert>
</div> </v-card-text>
</v-card>
</template> </template>
<script> <script>
@ -128,13 +100,18 @@ export default {
} catch { } catch {
this.error = true; this.error = true;
} }
if (key.status != 200) this.error = true; if (key.status != 200) {
else { this.error = true;
this.$emit("logged-in"); this.loading = false;
} else {
this.clear(); 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; this.loading = false;
}, },
}, },

View file

@ -76,7 +76,6 @@
<v-btn color="success" @click="save" text :disabled="meals.length == 0"> <v-btn color="success" @click="save" text :disabled="meals.length == 0">
{{ $t("general.save") }} {{ $t("general.save") }}
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-row> </v-row>
</v-card> </v-card>

View 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>

View file

@ -31,7 +31,7 @@
</v-row> </v-row>
</v-card-text> </v-card-text>
</v-card> </v-card>
<v-row> <v-row v-if="!viewScale">
<v-col <v-col
:sm="6" :sm="6"
:md="6" :md="6"
@ -49,14 +49,36 @@
/> />
</v-col> </v-col>
</v-row> </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> </div>
</template> </template>
<script> <script>
import RecipeCard from "../Recipe/RecipeCard"; import RecipeCard from "../Recipe/RecipeCard";
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
export default { export default {
components: { components: {
RecipeCard, RecipeCard,
MobileRecipeCard,
}, },
props: { props: {
sortable: { sortable: {
@ -68,8 +90,17 @@ export default {
default: 6, default: 6,
}, },
}, },
data() { computed: {
return {}; viewScale() {
switch (this.$vuetify.breakpoint.name) {
case "xs":
return true;
case "sm":
return true;
default:
return false;
}
},
}, },
}; };
</script> </script>

View file

@ -2,8 +2,8 @@
<v-form ref="file"> <v-form ref="file">
<input ref="uploader" class="d-none" type="file" @change="onFileChanged" /> <input ref="uploader" class="d-none" type="file" @change="onFileChanged" />
<v-btn :loading="isSelecting" @click="onButtonClick" color="accent" text> <v-btn :loading="isSelecting" @click="onButtonClick" color="accent" text>
<v-icon left> mdi-cloud-upload </v-icon> <v-icon left> {{ icon }}</v-icon>
{{ $t("general.upload") }} {{ text ? text : defaultText }}
</v-btn> </v-btn>
</v-form> </v-form>
</template> </template>
@ -13,18 +13,27 @@ import api from "@/api";
export default { export default {
props: { props: {
url: String, url: String,
text: { default: "Upload" },
icon: { default: "mdi-cloud-upload" },
fileName: { defaul: "archive" },
}, },
data: () => ({ data: () => ({
file: null, file: null,
isSelecting: false, isSelecting: false,
}), }),
computed: {
defaultText() {
return this.$t("general.upload");
},
},
methods: { methods: {
async upload() { async upload() {
if (this.file != null) { if (this.file != null) {
this.isSelecting = true; this.isSelecting = true;
let formData = new FormData(); let formData = new FormData();
formData.append("archive", this.file); formData.append(this.fileName, this.file);
await api.utils.uploadFile(this.url, formData); await api.utils.uploadFile(this.url, formData);

View file

@ -1,7 +1,7 @@
import Vue from "vue"; import Vue from "vue";
import App from "./App.vue"; import App from "./App.vue";
import vuetify from "./plugins/vuetify"; import vuetify from "./plugins/vuetify";
import store from "./store/store"; import store from "./store";
import VueRouter from "vue-router"; import VueRouter from "vue-router";
import { routes } from "./routes"; import { routes } from "./routes";
import i18n from "./i18n"; import i18n from "./i18n";

View 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;
},
},
};

View 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;
},
},
};

View 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",
};
},
};

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="text-center"> <v-container class="text-center">
<v-row> <v-row>
<v-col cols="2"></v-col> <v-col cols="2"></v-col>
<v-col> <v-col>
@ -12,7 +12,7 @@
</v-col> </v-col>
<v-col cols="2"></v-col> <v-col cols="2"></v-col>
</v-row> </v-row>
</div> </v-container>
</template> </template>
<script> <script>

View file

@ -33,18 +33,22 @@
</v-app-bar> </v-app-bar>
<v-card-text> <v-card-text>
<v-container> <v-form ref="newUser">
<v-row> <v-row>
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
<v-text-field <v-text-field
v-model="editedItem.fullName" v-model="editedItem.fullName"
label="Full Name" label="Full Name"
:rules="[existsRule]"
validate-on-blur
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
<v-text-field <v-text-field
v-model="editedItem.email" v-model="editedItem.email"
label="Email" label="Email"
:rules="[existsRule, emailRule]"
validate-on-blur
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
@ -57,6 +61,7 @@
<v-text-field <v-text-field
v-model="editedItem.password" v-model="editedItem.password"
label="User Password" label="User Password"
:rules="[existsRule, minRule]"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="12" md="3"> <v-col cols="12" sm="12" md="3">
@ -66,7 +71,7 @@
></v-switch> ></v-switch>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-form>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
@ -121,8 +126,10 @@
<script> <script>
import Confirmation from "@/components/UI/Confirmation"; import Confirmation from "@/components/UI/Confirmation";
import api from "@/api"; import api from "@/api";
import { validators } from "@/mixins/validators";
export default { export default {
components: { Confirmation }, components: { Confirmation },
mixins: [validators],
data: () => ({ data: () => ({
dialog: false, dialog: false,
activeId: null, activeId: null,
@ -145,6 +152,7 @@ export default {
editedItem: { editedItem: {
id: 0, id: 0,
fullName: "", fullName: "",
password: "",
email: "", email: "",
family: "", family: "",
admin: false, admin: false,
@ -152,6 +160,7 @@ export default {
defaultItem: { defaultItem: {
id: 0, id: 0,
fullName: "", fullName: "",
password: "",
email: "", email: "",
family: "", family: "",
admin: false, admin: false,
@ -186,7 +195,7 @@ export default {
}, },
async deleteUser() { async deleteUser() {
await api.users.delete(this.editedIndex); await api.users.delete(this.activeId);
this.initialize(); this.initialize();
}, },
@ -227,13 +236,13 @@ export default {
async save() { async save() {
if (this.editedIndex > -1) { if (this.editedIndex > -1) {
console.log("New User", this.editedItem);
api.users.update(this.editedItem); api.users.update(this.editedItem);
} else { this.close();
} else if (this.$refs.newUser.validate()) {
api.users.create(this.editedItem); api.users.create(this.editedItem);
this.close();
} }
await this.initialize(); await this.initialize();
this.close();
}, },
}, },
}; };

View file

@ -1,59 +1,168 @@
<template> <template>
<v-card> <v-row dense>
<v-card-title class="headline"> <v-col cols="12" md="8" sm="12">
<span> <v-card>
<v-avatar color="accent" size="40" class="mr-2" v-if="!loading"> <v-card-title class="headline">
<img src="https://cdn.vuetifyjs.com/images/john.jpg" alt="John" /> <span>
</v-avatar> <v-progress-circular
<v-progress-circular v-if="loading"
v-else indeterminate
indeterminate color="primary"
color="primary" large
large class="mr-2"
class="mr-2" >
> </v-progress-circular>
</v-progress-circular> </span>
</span> Profile
Profile <v-spacer></v-spacer>
<v-spacer></v-spacer> User ID: {{ user.id }}
User ID: {{ user.id }} </v-card-title>
</v-card-title> <v-divider></v-divider>
<v-divider></v-divider> <v-card-text>
<v-card-text> <v-row>
<v-form> <v-col cols="12" md="3" align="center" justify="center">
<v-text-field label="Full Name" v-model="user.fullName"> </v-text-field> <v-avatar
<v-text-field label="Email" v-model="user.email"> </v-text-field> color="accent"
<v-text-field size="120"
label="Family" class="white--text headline mr-2"
readonly >
v-model="user.family" <img
persistent-hint :src="userProfileImage"
hint="Family groups can only be set by administrators" v-if="!hideImage"
> @error="hideImage = true"
</v-text-field> />
</v-form> <div v-else>
</v-card-text> {{ initials }}
<v-card-actions> </div>
<v-btn color="accent" class="mr-2"> </v-avatar>
<v-icon left> mdi-lock </v-icon> </v-col>
{{ $t("settings.change-password") }} <v-col cols="12" md="9">
</v-btn> <v-form>
<v-spacer></v-spacer> <v-text-field
<v-btn color="success" class="mr-2" @click="updateUser"> label="Full Name"
<v-icon left> mdi-content-save </v-icon> required
{{ $t("general.save") }} v-model="user.fullName"
</v-btn> :rules="[existsRule]"
</v-card-actions> validate-on-blur
</v-card> >
</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> </template>
<script> <script>
// import AvatarPicker from '@/components/AvatarPicker' // import AvatarPicker from '@/components/AvatarPicker'
import UploadBtn from "@/components/UI/UploadBtn";
import api from "@/api"; import api from "@/api";
import { validators } from "@/mixins/validators";
import { initials } from "@/mixins/initials";
export default { export default {
pageTitle: "My Profile", components: {
UploadBtn,
},
mixins: [validators, initials],
data() { data() {
return { return {
hideImage: false,
passwordLoading: false,
password: {
current: "",
newOne: "",
newTwo: "",
},
showPassword: false,
loading: false, loading: false,
user: { user: {
fullName: "Change Me", fullName: "Change Me",
@ -62,10 +171,15 @@ export default {
admin: true, admin: true,
id: 1, id: 1,
}, },
showAvatarPicker: false,
}; };
}, },
computed: {
userProfileImage() {
return `api/users/${this.user.id}/image`;
},
},
async mounted() { async mounted() {
this.refreshProfile(); this.refreshProfile();
}, },
@ -87,6 +201,21 @@ export default {
this.refreshProfile(); this.refreshProfile();
this.loading = false; 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>

View file

@ -1,18 +1,73 @@
<template> <template>
<div> <v-container>
<AdminSidebar /> <AdminSidebar />
<v-slide-x-transition hide-on-leave> <v-slide-x-transition hide-on-leave>
<router-view></router-view> <router-view></router-view>
</v-slide-x-transition> </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> </template>
<script> <script>
import AdminSidebar from "../../components/Admin/AdminSidebar"; import AdminSidebar from "@/components/Admin/AdminSidebar";
import axios from "axios";
import api from "@/api";
export default { export default {
components: { components: {
AdminSidebar, 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> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div> <v-container>
<CategorySidebar /> <CategorySidebar />
<CardSection <CardSection
:sortable="true" :sortable="true"
@ -9,7 +9,7 @@
@sort="sortAZ" @sort="sortAZ"
@sort-recent="sortRecent" @sort-recent="sortRecent"
/> />
</div> </v-container>
</template> </template>
<script> <script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div> <v-container>
<CategorySidebar /> <CategorySidebar />
<CardSection <CardSection
:sortable="true" :sortable="true"
@ -9,7 +9,7 @@
@sort="sortAZ" @sort="sortAZ"
@sort-recent="sortRecent" @sort-recent="sortRecent"
/> />
</div> </v-container>
</template> </template>
<script> <script>

View file

@ -1,8 +1,8 @@
<template> <template>
<div> <v-container>
<LastRecipe /> <LastRecipe />
<LogFile class="mt-2" /> <LogFile class="mt-2" />
</div> </v-container>
</template> </template>
<script> <script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div> <v-container>
<CategorySidebar /> <CategorySidebar />
<CardSection <CardSection
v-if="showRecent" v-if="showRecent"
@ -17,7 +17,7 @@
@sort="sortAZ(index)" @sort="sortAZ(index)"
@sort-recent="sortRecent(index)" @sort-recent="sortRecent(index)"
/> />
</div> </v-container>
</template> </template>
<script> <script>

View file

@ -1,9 +1,12 @@
<template> <template>
<v-row justify="start" height="100%"> <v-container fill-height class="text-center">
<v-col align="center"> <v-row>
<LoginForm /> <v-flex class="d-flex justify-center" width="500px">
</v-col> <LoginForm @logged-in="redirectMe" class="ma-1" />
</v-row> </v-flex>
</v-row>
<v-row></v-row>
</v-container>
</template> </template>
<script> <script>
@ -12,6 +15,25 @@ export default {
components: { components: {
LoginForm, 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> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div> <v-container>
<EditPlan <EditPlan
v-if="editMealPlan" v-if="editMealPlan"
:meal-plan="editMealPlan" :meal-plan="editMealPlan"
@ -25,8 +25,8 @@
> >
<v-card class="mt-1"> <v-card class="mt-1">
<v-card-title> <v-card-title>
{{ $d( new Date(mealplan.startDate), 'short' ) }} - {{ $d(new Date(mealplan.startDate), "short") }} -
{{ $d( new Date(mealplan.endDate), 'short' ) }} {{ $d(new Date(mealplan.endDate), "short") }}
</v-card-title> </v-card-title>
<v-list nav> <v-list nav>
<v-list-item-group color="primary"> <v-list-item-group color="primary">
@ -43,7 +43,9 @@
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<v-list-item-title v-text="meal.name"></v-list-item-title> <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-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
@ -79,7 +81,7 @@
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
</div> </v-container>
</template> </template>
<script> <script>

View file

@ -37,7 +37,7 @@
</v-card> </v-card>
</v-col> </v-col>
<v-col order-sm="0" :order-md="getOrder(index)" md="6" sm="12"> <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-img :src="getImage(meal.image)" max-height="300"> </v-img>
</v-card> </v-card>
</v-col> </v-col>

View file

@ -1,41 +1,43 @@
<template> <template>
<v-card :loading="isLoading"> <v-container>
<v-img v-if="image" height="400" :src="image"> <v-card :loading="isLoading">
<template v-slot:placeholder> <v-img v-if="image" height="400" :src="image">
<v-row class="fill-height ma-0" align="center" justify="center"> <template v-slot:placeholder>
<v-progress-circular <v-row class="fill-height ma-0" align="center" justify="center">
indeterminate <v-progress-circular
color="grey lighten-5" indeterminate
></v-progress-circular> color="grey lighten-5"
</v-row> ></v-progress-circular>
</template> </v-row>
</v-img> </template>
<br v-else /> </v-img>
<br v-else />
<EditorButtonRow <EditorButtonRow
@json="jsonEditor = true" @json="jsonEditor = true"
@editor="jsonEditor = false" @editor="jsonEditor = false"
@save="createRecipe" @save="createRecipe"
/>
<div v-if="jsonEditor">
<!-- Probably not the best way, but it works! -->
<br />
<br />
<VJsoneditor
v-model="recipeDetails"
height="1500px"
:options="jsonEditorOptions"
/> />
</div>
<RecipeEditor <div v-if="jsonEditor">
ref="recipeEditor" <!-- Probably not the best way, but it works! -->
v-else <br />
v-model="recipeDetails" <br />
@upload="getImage" <VJsoneditor
/> v-model="recipeDetails"
</v-card> height="1500px"
:options="jsonEditorOptions"
/>
</div>
<RecipeEditor
ref="recipeEditor"
v-else
v-model="recipeDetails"
@upload="getImage"
/>
</v-card>
</v-container>
</template> </template>
<script> <script>

View file

@ -1,58 +1,61 @@
<template> <template>
<v-card id="myRecipe"> <v-container>
<v-img <v-card id="myRecipe">
height="400" <v-img
:src="getImage(recipeDetails.image)" height="400"
class="d-print-none" :src="getImage(recipeDetails.image)"
:key="imageKey" class="d-print-none"
> :key="imageKey"
<RecipeTimeCard >
class="force-bottom" <RecipeTimeCard
:prepTime="recipeDetails.prepTime" class="force-bottom"
:totalTime="recipeDetails.totalTime" :prepTime="recipeDetails.prepTime"
:performTime="recipeDetails.performTime" :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 <RecipeViewer
v-if="!form" v-if="!form"
:name="recipeDetails.name" :name="recipeDetails.name"
:ingredients="recipeDetails.recipeIngredient" :ingredients="recipeDetails.recipeIngredient"
:description="recipeDetails.description" :description="recipeDetails.description"
:instructions="recipeDetails.recipeInstructions" :instructions="recipeDetails.recipeInstructions"
:tags="recipeDetails.tags" :tags="recipeDetails.tags"
:categories="recipeDetails.categories" :categories="recipeDetails.categories"
:notes="recipeDetails.notes" :notes="recipeDetails.notes"
:rating="recipeDetails.rating" :rating="recipeDetails.rating"
:yields="recipeDetails.recipeYield" :yields="recipeDetails.recipeYield"
:orgURL="recipeDetails.orgURL" :orgURL="recipeDetails.orgURL"
/> />
<VJsoneditor <VJsoneditor
@error="logError()" @error="logError()"
class="mt-10" class="mt-10"
v-else-if="showJsonEditor" v-else-if="showJsonEditor"
v-model="recipeDetails" v-model="recipeDetails"
height="1500px" height="1500px"
:options="jsonEditorOptions" :options="jsonEditorOptions"
/> />
<RecipeEditor <RecipeEditor
v-else v-else
v-model="recipeDetails" v-model="recipeDetails"
ref="recipeEditor" ref="recipeEditor"
@upload="getImageFile" @upload="getImageFile"
/> />
</v-card> </v-card>
</v-container>
</template> </template>
<script> <script>
@ -63,6 +66,7 @@ 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 EditorButtonRow from "../components/Recipe/EditorButtonRow"; import EditorButtonRow from "../components/Recipe/EditorButtonRow";
import { user } from "@/mixins/user";
export default { export default {
components: { components: {
@ -72,6 +76,7 @@ export default {
EditorButtonRow, EditorButtonRow,
RecipeTimeCard, RecipeTimeCard,
}, },
mixins: [user],
data() { data() {
return { return {
// currentRecipe: this.$route.params.recipe, // currentRecipe: this.$route.params.recipe,

View file

@ -1,5 +1,5 @@
<template> <template>
<div> <v-container>
<v-row justify="center"> <v-row justify="center">
<v-col cols="1"> </v-col> <v-col cols="1"> </v-col>
<v-col> <v-col>
@ -30,7 +30,7 @@
/> />
</v-col> </v-col>
</v-row> </v-row>
</div> </v-container>
</template> </template>
<script> <script>

View file

@ -6,10 +6,16 @@ import Migration from "@/pages/Admin/Migration";
import Profile from "@/pages/Admin/Profile"; import Profile from "@/pages/Admin/Profile";
import ManageUsers from "@/pages/Admin/ManageUsers"; import ManageUsers from "@/pages/Admin/ManageUsers";
import Settings from "@/pages/Admin/Settings"; import Settings from "@/pages/Admin/Settings";
import { store } from "../store";
export default { export default {
path: "/admin", path: "/admin",
component: Admin, component: Admin,
beforeEnter: (to, _from, next) => {
if (store.getters.getIsLoggedIn) {
next();
} else next({ path: "/login", query: { redirect: to.fullPath } });
},
children: [ children: [
{ {
path: "", path: "",

View file

@ -11,7 +11,7 @@ import LoginPage from "../pages/LoginPage";
import MealPlanThisWeekPage from "../pages/MealPlanThisWeekPage"; import MealPlanThisWeekPage from "../pages/MealPlanThisWeekPage";
import api from "@/api"; import api from "@/api";
import Admin from "./admin"; import Admin from "./admin";
import { store } from "../store/store"; import { store } from "../store";
export const routes = [ export const routes = [
{ path: "/", name: "home", component: HomePage }, { path: "/", name: "home", component: HomePage },

View file

@ -21,6 +21,7 @@ const state = {
isDark: false, isDark: false,
isLoggedIn: false, isLoggedIn: false,
token: "", token: "",
userData: {},
}; };
const mutations = { const mutations = {
@ -46,6 +47,10 @@ const mutations = {
axios.defaults.headers.common["Authorization"] = `Bearer ${payload}`; axios.defaults.headers.common["Authorization"] = `Bearer ${payload}`;
state.token = payload; state.token = payload;
}, },
setUserData(state, payload) {
state.userData = payload;
},
}; };
const actions = { const actions = {
@ -77,6 +82,7 @@ const getters = {
getIsDark: state => state.isDark, getIsDark: state => state.isDark,
getIsLoggedIn: state => state.isLoggedIn, getIsLoggedIn: state => state.isLoggedIn,
getToken: state => state.token, getToken: state => state.token,
getUserData: state => state.userData,
}; };
export default { export default {

View file

@ -1,7 +1,5 @@
// import utils from "@/utils"; import { vueApp } from "../main";
// import Vue from "vue";
// import Vuetify from "./plugins/vuetify";
import { vueApp } from "./main";
const notifyHelpers = { const notifyHelpers = {
baseCSS: "notify-base", baseCSS: "notify-base",

View file

@ -12,7 +12,7 @@ def ensure_dirs():
# Register ENV # Register ENV
ENV = CWD.joinpath(".env") ENV = CWD.joinpath(".env") #! I'm Broken Fix Me!
dotenv.load_dotenv(ENV) dotenv.load_dotenv(ENV)
@ -45,7 +45,9 @@ MIGRATION_DIR = DATA_DIR.joinpath("migration")
NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud") NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud")
CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown") CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown")
TEMPLATE_DIR = DATA_DIR.joinpath("templates") TEMPLATE_DIR = DATA_DIR.joinpath("templates")
USER_DIR = DATA_DIR.joinpath("users")
SQLITE_DIR = DATA_DIR.joinpath("db") SQLITE_DIR = DATA_DIR.joinpath("db")
RECIPE_DATA_DIR = DATA_DIR.joinpath("recipes")
TEMP_DIR = DATA_DIR.joinpath(".temp") TEMP_DIR = DATA_DIR.joinpath(".temp")
REQUIRED_DIRS = [ REQUIRED_DIRS = [
@ -58,6 +60,8 @@ REQUIRED_DIRS = [
SQLITE_DIR, SQLITE_DIR,
NEXTCLOUD_DIR, NEXTCLOUD_DIR,
CHOWDOWN_DIR, CHOWDOWN_DIR,
RECIPE_DATA_DIR,
USER_DIR,
] ]
ensure_dirs() ensure_dirs()

View file

@ -61,6 +61,15 @@ class _Users(BaseDocument):
self.primary_key = "id" self.primary_key = "id"
self.sql_model = User 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: class Database:
def __init__(self) -> None: def __init__(self) -> None:

View file

@ -42,3 +42,6 @@ class User(SqlAlchemyBase, BaseMixins):
self.email = email self.email = email
self.family = family self.family = family
self.admin = admin self.admin = admin
def update_password(self, password):
self.password = password

View file

@ -79,7 +79,7 @@ def filter_by_category(categories: list, session: Session = Depends(generate_ses
in_category = [ in_category = [
db.categories.get(session, slugify(cat), limit=1) for cat in categories 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] in_category = [item for sublist in in_category for item in sublist]
return in_category return in_category

View file

@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from fastapi_login.exceptions import InvalidCredentialsException from fastapi_login.exceptions import InvalidCredentialsException
from routes.deps import manager, query_user from routes.deps import manager, query_user
from schema.snackbar import SnackResponse
from schema.user import UserInDB from schema.user import UserInDB
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -13,7 +14,7 @@ router = APIRouter(prefix="/api/auth", tags=["Auth"])
@router.post("/token") @router.post("/token")
def token( def get_token(
data: OAuth2PasswordRequestForm = Depends(), data: OAuth2PasswordRequestForm = Depends(),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
@ -29,4 +30,42 @@ def token(
access_token = manager.create_access_token( access_token = manager.create_access_token(
data=dict(sub=email), expires=timedelta(hours=2) 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"} return {"access_token": access_token, "token_type": "bearer"}

View file

@ -1,11 +1,15 @@
import shutil
from datetime import timedelta 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.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, File, UploadFile
from routes.deps import manager, query_user from fastapi.responses import FileResponse
from schema.user import UserBase, UserIn, UserInDB, UserOut 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 from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users", tags=["Users"]) router = APIRouter(prefix="/api/users", tags=["Users"])
@ -17,12 +21,11 @@ async def create_user(
current_user=Depends(manager), current_user=Depends(manager),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Returns a list of all user in the Database """
new_user.password = get_password_hash(new_user.password) new_user.password = get_password_hash(new_user.password)
data = db.users.create(session, new_user.dict()) 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]) @router.get("", response_model=list[UserOut])
@ -38,7 +41,7 @@ async def get_all_users(
@router.get("/self", response_model=UserOut) @router.get("/self", response_model=UserOut)
async def get_user_by_id( async def get_logged_in_user(
current_user: UserInDB = Depends(manager), current_user: UserInDB = Depends(manager),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
@ -69,8 +72,71 @@ async def update_user(
access_token = manager.create_access_token( access_token = manager.create_access_token(
data=dict(sub=email), expires=timedelta(hours=2) data=dict(sub=email), expires=timedelta(hours=2)
) )
return {"access_token": access_token, "token_type": "bearer"} access_token = {"access_token": access_token, "token_type": "bearer"}
return
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}") @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""" """ 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: 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")

View file

@ -5,6 +5,11 @@ from fastapi_camelcase import CamelModel
# from pydantic import EmailStr # from pydantic import EmailStr
class ChangePassword(CamelModel):
current_password: str
new_password: str
class UserBase(CamelModel): class UserBase(CamelModel):
full_name: Optional[str] = None full_name: Optional[str] = None
email: str email: str

View file

@ -6,16 +6,17 @@ from pathlib import Path
from core.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 fastapi.logger import logger
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 fastapi.logger import logger
class ExportDatabase: class ExportDatabase:
def __init__(self, session, tag=None, templates=None) -> None: def __init__(self, session, tag=None, templates=None) -> None:
"""Export a Mealie database. Export interacts directly with class objects and can be used """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: Args:

249
poetry.lock generated
View file

@ -263,6 +263,14 @@ fastapi = "*"
passlib = "*" passlib = "*"
pyjwt = "*" 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]] [[package]]
name = "h11" name = "h11"
version = "0.12.0" version = "0.12.0"
@ -365,6 +373,14 @@ MarkupSafe = ">=0.23"
[package.extras] [package.extras]
i18n = ["Babel (>=0.8)"] 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]] [[package]]
name = "jstyleson" name = "jstyleson"
version = "0.0.2" version = "0.0.2"
@ -381,6 +397,34 @@ category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 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]] [[package]]
name = "lxml" name = "lxml"
version = "4.6.2" version = "4.6.2"
@ -395,6 +439,17 @@ html5 = ["html5lib"]
htmlsoup = ["beautifulsoup4"] htmlsoup = ["beautifulsoup4"]
source = ["Cython (>=0.29.7)"] 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]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "1.1.1" version = "1.1.1"
@ -424,6 +479,49 @@ BeautifulSoup4 = ">=4.6.0"
html5lib = ">=1.0.1" html5lib = ">=1.0.1"
requests = ">=2.18.4" 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]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "0.4.3" version = "0.4.3"
@ -432,6 +530,28 @@ category = "dev"
optional = false optional = false
python-versions = "*" 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]] [[package]]
name = "packaging" name = "packaging"
version = "20.8" version = "20.8"
@ -505,6 +625,14 @@ dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"] email = ["email-validator (>=1.0.3)"]
typing_extensions = ["typing-extensions (>=3.7.2)"] 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]] [[package]]
name = "pyhumps" name = "pyhumps"
version = "1.6.1" version = "1.6.1"
@ -542,6 +670,17 @@ isort = ">=4.2.5,<6"
mccabe = ">=0.6,<0.7" mccabe = ">=0.6,<0.7"
toml = ">=0.7.1" 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]] [[package]]
name = "pyparsing" name = "pyparsing"
version = "2.4.7" version = "2.4.7"
@ -783,6 +922,26 @@ category = "dev"
optional = false optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 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]] [[package]]
name = "typed-ast" name = "typed-ast"
version = "1.4.2" version = "1.4.2"
@ -914,7 +1073,7 @@ python-versions = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "182496243ce59b60506b646a0a521d0186a7ef36009c5212276807e13e20d8a3" content-hash = "adf8ad8e07d1af5c231c936276b57be83dd534e0cf706042ddb26f6ff51c86ca"
[metadata.files] [metadata.files]
aiofiles = [ aiofiles = [
@ -1087,6 +1246,9 @@ fastapi-login = [
{file = "fastapi-login-1.5.3.tar.gz", hash = "sha256:8e8ef710f1b7107e81d00e205779e73e17be35d5a91d11685ff72f323898e93b"}, {file = "fastapi-login-1.5.3.tar.gz", hash = "sha256:8e8ef710f1b7107e81d00e205779e73e17be35d5a91d11685ff72f323898e93b"},
{file = "fastapi_login-1.5.3-py3-none-any.whl", hash = "sha256:6c83b74bdb45c34ec0aab22000a7951df96c5d011f02a99a46ca4b2be6b1263c"}, {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 = [ h11 = [
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, {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-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, {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 = [ jstyleson = [
{file = "jstyleson-0.0.2.tar.gz", hash = "sha256:680003f3b15a2959e4e6a351f3b858e3c07dd3e073a0d54954e34d8ea5e1308e"}, {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-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"},
{file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"}, {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 = [ lxml = [
{file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a9d6bc8642e2c67db33f1247a77c53476f3a166e09067c0474facb045756087f"}, {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"}, {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-cp39-cp39-win_amd64.whl", hash = "sha256:535332fe9d00c3cd455bd3dd7d4bacab86e2d564bdf7606079160fa6251caacf"},
{file = "lxml-4.6.2.tar.gz", hash = "sha256:cd11c7e8d21af997ee8079037fff88f16fda188a9776eb4b81c7e4c9c0a7d7fc"}, {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 = [ markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, {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"}, {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
@ -1259,10 +1436,25 @@ mccabe = [
mf2py = [ mf2py = [
{file = "mf2py-1.1.2.tar.gz", hash = "sha256:84f1f8f2ff3f1deb1c30be497e7ccd805452996a662fd4a77f09e0105bede2c9"}, {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 = [ mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {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"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
] ]
nltk = [
{file = "nltk-3.5.zip", hash = "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35"},
]
packaging = [ packaging = [
{file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"},
{file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, {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-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"},
{file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"}, {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 = [ pyhumps = [
{file = "pyhumps-1.6.1-py3-none-any.whl", hash = "sha256:58b367b73c57b64e32d211dc769addabd68ff6db07ce64b2e6565f7d5a12291f"}, {file = "pyhumps-1.6.1-py3-none-any.whl", hash = "sha256:58b367b73c57b64e32d211dc769addabd68ff6db07ce64b2e6565f7d5a12291f"},
{file = "pyhumps-1.6.1.tar.gz", hash = "sha256:01612603c5ad73a407299d806d30708a3935052276fdd93776953bccc0724e0a"}, {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-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"},
{file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"}, {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 = [ pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, {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-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, {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 = [ 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_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},

View file

@ -26,14 +26,15 @@ extruct = "^0.12.0"
scrape-schema-recipe = "^0.1.3" scrape-schema-recipe = "^0.1.3"
python-multipart = "^0.0.5" python-multipart = "^0.0.5"
fastapi-login = "^1.5.3" fastapi-login = "^1.5.3"
bcrypt = "^3.2.0"
fastapi-camelcase = "^1.0.2" fastapi-camelcase = "^1.0.2"
bcrypt = "^3.2.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pylint = "^2.6.0" pylint = "^2.6.0"
black = "^20.8b1" black = "^20.8b1"
pytest = "^6.2.1" pytest = "^6.2.1"
pytest-cov = "^2.11.0" pytest-cov = "^2.11.0"
mkdocs-material = "^7.0.2"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]