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/img/*
app_data/migration/*
app_data/users/*
#Exception to keep folders
!mealie/dist/.gitkeep

View file

@ -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"],

View file

@ -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
![](../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.

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:
- 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"

View file

@ -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",

View file

@ -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;

View file

@ -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"

View file

@ -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/";

View file

@ -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 {

View file

@ -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";

View file

@ -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";

View file

@ -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;

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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);

View file

@ -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";

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

View file

@ -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();
},
},
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,

View file

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

View file

@ -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: "",

View file

@ -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 },

View file

@ -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 {

View file

@ -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",

View file

@ -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()

View file

@ -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:

View file

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

View file

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

View file

@ -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"}

View file

@ -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")

View file

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

View file

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

@ -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"},

View file

@ -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"]