fix(backend): 🐛 Grab PRs from dev branch (#826)
* fix(backend): 🐛 Grab PR #780 * feat(frontend): ✨ Grab PR 797 * docs(docs): spelling * feat(backend): ✨ Add LDAP Support from #803 * add test deps Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
dce84c3937
commit
0db8a58963
11 changed files with 550 additions and 309 deletions
1
.github/workflows/backend-tests.yml
vendored
1
.github/workflows/backend-tests.yml
vendored
|
@ -55,6 +55,7 @@ jobs:
|
|||
#----------------------------------------------
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install libsasl2-dev libldap2-dev libssl-dev
|
||||
poetry install
|
||||
poetry add "psycopg2-binary==2.8.6"
|
||||
# if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
| TZ | UTC | Must be set to get correct date/time on the server |
|
||||
|
||||
|
||||
|
||||
|
||||
### Database
|
||||
|
||||
| Variables | Default | Description |
|
||||
|
@ -49,3 +51,13 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
|
|||
| WORKERS_PER_CORE | 1 | Set the number of workers to the number of CPU cores multiplied by this value (Value \* CPUs). More info [here][workers_per_core] |
|
||||
| MAX_WORKERS | 1 | Set the maximum number of workers to use. Default is not set meaning unlimited. More info [here][max_workers] |
|
||||
| WEB_CONCURRENCY | 1 | Override the automatic definition of number of workers. More info [here][web_concurrency] |
|
||||
|
||||
|
||||
### LDAP
|
||||
|
||||
| Variables | Default | Description |
|
||||
| ------------------ | :-----: | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| LDAP_AUTH_ENABLED | False | Authenticate via an external LDAP server in addidion to built-in Mealie auth |
|
||||
| LDAP_SERVER_URL | None | LDAP server URL (e.g. ldap://ldap.example.com) |
|
||||
| LDAP_BIND_TEMPLATE | None | Templated DN for users, `{}` will be replaced with the username (e.g. `cn={},dc=example,dc=com`) |
|
||||
| LDAP_ADMIN_FILTER | None | Optional LDAP filter, which tells Mealie the LDAP user is an admin (e.g. `(memberOf=cn=admins,dc=example,dc=com)`) |
|
|
@ -1,355 +1,421 @@
|
|||
<!-- Custom HTML site displayed as the Home chapter -->
|
||||
|
||||
{% extends "main.html" %}
|
||||
{% block tabs %}
|
||||
{{ super() }}
|
||||
{% extends "main.html" %} {% block tabs %} {{ super() }}
|
||||
<style>
|
||||
.md-main {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.md-main {
|
||||
flex-grow: 0
|
||||
.md-main__inner {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tx-container {
|
||||
padding-top: 0rem;
|
||||
}
|
||||
|
||||
.tx-hero {
|
||||
margin: 12px 2.8rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tx-hero h1 {
|
||||
margin-bottom: 1rem;
|
||||
font-family: "Roboto";
|
||||
color: var(--md-custom-h2-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tx-hero__content {
|
||||
padding-bottom: 1rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tx-hero__image {
|
||||
order: 1;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.tx-hero .md-button {
|
||||
margin-top: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
color: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
.tx-hero .md-button--primary {
|
||||
background-color: var(--md-primary--color);
|
||||
border-color: var(--md-primary-bg-color);
|
||||
}
|
||||
|
||||
.tx-hero .md-button:focus,
|
||||
.tx-hero .md-button:hover {
|
||||
background-color: var(--md-accent-fg-color);
|
||||
color: var(--md-default-bg-color);
|
||||
border-color: var(--md-accent-fg-color);
|
||||
}
|
||||
|
||||
.feature-item h2 svg {
|
||||
height: 30px;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
|
||||
.feature-container {
|
||||
background-color: var(--md-default-accent-bg-color);
|
||||
}
|
||||
|
||||
.top-hr {
|
||||
margin-top: 42px;
|
||||
margin-bottom: 42px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-weight: 300;
|
||||
box-sizing: border-box;
|
||||
padding: 0 15px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.feature-item h2 {
|
||||
color: var(--md-custom-h2-color);
|
||||
font-weight: 300;
|
||||
font-size: 25px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: normal;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.feature-item p {
|
||||
font-size: 16px;
|
||||
line-height: 1.8em;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
color: var(--webkit-print-color-adjust);
|
||||
margin: 0 0 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 30em) {
|
||||
.tx-hero h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.md-main__inner {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tx-container {
|
||||
padding-top: .0rem;
|
||||
@media screen and (min-width: 60em) {
|
||||
.md-sidebar--secondary {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tx-hero {
|
||||
margin: 12px 2.8rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tx-hero h1 {
|
||||
margin-bottom: 1rem;
|
||||
font-family: "Roboto";
|
||||
color: var(--md-custom-h2-color);
|
||||
font-weight: 500
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tx-hero__content {
|
||||
padding-bottom: 1rem;
|
||||
margin: 0 auto;
|
||||
max-width: 22rem;
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 3.5rem;
|
||||
margin-left: 1rem;
|
||||
margin-right: 4rem;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tx-hero__image{
|
||||
|
||||
order:1;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.tx-hero .md-button {
|
||||
margin-top: .5rem;
|
||||
margin-right: .5rem;
|
||||
color: var(--md-primary-fg-color)
|
||||
}
|
||||
|
||||
.tx-hero .md-button--primary {
|
||||
background-color: var(--md-primary--color);
|
||||
border-color: var(--md-primary-bg-color)
|
||||
}
|
||||
|
||||
.tx-hero .md-button:focus,
|
||||
.tx-hero .md-button:hover {
|
||||
background-color: var(--md-accent-fg-color);
|
||||
color: var(--md-default-bg-color);
|
||||
border-color: var(--md-accent-fg-color)
|
||||
}
|
||||
|
||||
.feature-item h2 svg {
|
||||
height: 30px;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
|
||||
.feature-container {
|
||||
background-color: var(--md-default-accent-bg-color);
|
||||
@media screen and (min-width: 76.25em) {
|
||||
.md-sidebar--primary {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-hr {
|
||||
margin-top: 42px;
|
||||
margin-bottom: 42px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
max-width: 61rem;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding: 0 0.2rem;
|
||||
}
|
||||
|
||||
.bottom-hr {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
max-width: 61rem;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding: 0 0.2rem;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-weight: 300;
|
||||
box-sizing: border-box;
|
||||
padding: 0 15px;
|
||||
word-break: break-word
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.feature-item h2 {
|
||||
color: var(--md-custom-h2-color);
|
||||
font-weight: 300;
|
||||
font-size: 25px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: normal;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.hr {
|
||||
border-bottom: 1px solid #eee;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.feature-item p {
|
||||
font-size: 16px;
|
||||
line-height: 1.8em;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
color: var(--webkit-print-color-adjust);
|
||||
margin: 0 0 10px;
|
||||
display: block;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
margin-top: 15px;
|
||||
font-family: "Lato", sans-serif;
|
||||
font-size: 23px;
|
||||
font-weight: 300;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width:30em) {
|
||||
.tx-hero h1 {
|
||||
font-size: 1.4rem
|
||||
}
|
||||
}
|
||||
.logos {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-flow: row wrap;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width:60em) {
|
||||
.md-sidebar--secondary {
|
||||
display: none
|
||||
}
|
||||
.logos img {
|
||||
flex: 1 1 auto;
|
||||
padding: 25px;
|
||||
max-height: 130px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tx-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hr-logos {
|
||||
margin-top: 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.tx-hero__content {
|
||||
max-width: 22rem;
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 3.5rem;
|
||||
margin-left: 1.0rem;
|
||||
margin-right: 4.0rem;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.md-footer-meta__inner {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width:76.25em) {
|
||||
.md-sidebar--primary {
|
||||
display: none
|
||||
}
|
||||
|
||||
.top-hr {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
max-width: 61rem;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding: 0 .2rem;
|
||||
}
|
||||
|
||||
.bottom-hr {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
max-width: 61rem;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding: 0 .2rem;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hr {
|
||||
border-bottom: 1px solid #eee;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
margin-top: 15px;
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-size: 23px;
|
||||
font-weight: 300;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.logos {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-flow: row wrap;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logos img {
|
||||
flex: 1 1 auto;
|
||||
padding: 25px;
|
||||
max-height: 130px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.hr-logos {
|
||||
margin-top: 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.md-footer-meta__inner {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-top: 1.0rem;
|
||||
}
|
||||
|
||||
.md-footer-social {
|
||||
padding-top: 20px;
|
||||
}
|
||||
.md-footer-social {
|
||||
padding-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Main site Entry button descriptions -->
|
||||
<section class="tx-container">
|
||||
<div class="md-grid md-typeset">
|
||||
<div class="tx-hero">
|
||||
<div class="tx-hero__image">
|
||||
<img src="assets/img/home_screenshot.png" draggable="false">
|
||||
</div>
|
||||
<div class="tx-hero__content">
|
||||
<h1>
|
||||
Mealie
|
||||
</h1>
|
||||
<p>
|
||||
A self-hosted recipe manager and meal planner with a RestAPI backend and a
|
||||
reactive frontend application built in Vue for a pleasant user experience for the
|
||||
whole family.
|
||||
</p>
|
||||
<a href="{{ page.next_page.url | url }}" title="{{ page.next_page.title | striptags }}" class="md-button md-button--primary">
|
||||
Get started
|
||||
</a>
|
||||
<a href="{{ config.demo_url }}" title="{{ lang.t('source.link.title') }}" target="_blank" class="md-button">
|
||||
View the Demo
|
||||
</a>
|
||||
</div>
|
||||
<div class="md-grid md-typeset">
|
||||
<div class="tx-hero">
|
||||
<div class="tx-hero__image">
|
||||
<img src="assets/img/home_screenshot.png" draggable="false" />
|
||||
</div>
|
||||
<div class="tx-hero__content">
|
||||
<h1>Mealie</h1>
|
||||
<p>
|
||||
A self-hosted recipe manager and meal planner with a RestAPI backend
|
||||
and a reactive frontend application built in Vue for a pleasant user
|
||||
experience for the whole family.
|
||||
</p>
|
||||
<a
|
||||
href="{{ page.next_page.url | url }}"
|
||||
title="{{ page.next_page.title | striptags }}"
|
||||
class="md-button md-button--primary"
|
||||
>
|
||||
Get started
|
||||
</a>
|
||||
<a
|
||||
href="{{ config.demo_url }}"
|
||||
title="{{ lang.t('source.link.title') }}"
|
||||
target="_blank"
|
||||
class="md-button"
|
||||
>
|
||||
View the Demo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Main site box descriptions -->
|
||||
<!-- Row 1 -->
|
||||
<section class="feature-container">
|
||||
<div class="top-hr">
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M15.5,14L20.5,19L19,20.5L14,15.5V14.71L13.73,14.43C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.43,13.73L14.71,14H15.5M9.5,4.5L8.95,4.53C8.71,5.05 8.34,5.93 8.07,7H10.93C10.66,5.93 10.29,5.05 10.05,4.53C9.87,4.5 9.69,4.5 9.5,4.5M13.83,7C13.24,5.97 12.29,5.17 11.15,4.78C11.39,5.31 11.7,6.08 11.93,7H13.83M5.17,7H7.07C7.3,6.08 7.61,5.31 7.85,4.78C6.71,5.17 5.76,5.97 5.17,7M4.5,9.5C4.5,10 4.58,10.53 4.73,11H6.87L6.75,9.5L6.87,8H4.73C4.58,8.47 4.5,9 4.5,9.5M14.27,11C14.42,10.53 14.5,10 14.5,9.5C14.5,9 14.42,8.47 14.27,8H12.13C12.21,8.5 12.25,9 12.25,9.5C12.25,10 12.21,10.5 12.13,11H14.27M7.87,8L7.75,9.5L7.87,11H11.13C11.21,10.5 11.25,10 11.25,9.5C11.25,9 11.21,8.5 11.13,8H7.87M9.5,14.5C9.68,14.5 9.86,14.5 10.03,14.47C10.28,13.95 10.66,13.07 10.93,12H8.07C8.34,13.07 8.72,13.95 8.97,14.47L9.5,14.5M13.83,12H11.93C11.7,12.92 11.39,13.69 11.15,14.22C12.29,13.83 13.24,13.03 13.83,12M5.17,12C5.76,13.03 6.71,13.83 7.85,14.22C7.61,13.69 7.3,12.92 7.07,12H5.17Z"
|
||||
/>
|
||||
</svg>
|
||||
Import Recipes
|
||||
</h2>
|
||||
<p>
|
||||
Quickly and easily import recipes from sites around the web using the
|
||||
built in <b>recipe scraper</b>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,3A9,9 0 0,0 3,12H0L4,16L8,12H5A7,7 0 0,1 12,5A7,7 0 0,1 19,12A7,7 0 0,1 12,19C10.5,19 9.09,18.5 7.94,17.7L6.5,19.14C8.04,20.3 9.94,21 12,21A9,9 0 0,0 21,12A9,9 0 0,0 12,3M14,12A2,2 0 0,0 12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12Z"
|
||||
/>
|
||||
</svg>
|
||||
Automatic Backups
|
||||
</h2>
|
||||
<p>
|
||||
Keep your data safe with automatic backups in any format supported by
|
||||
<b>Jinja2</b> templates
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19,4C20.11,4 21,4.9 21,6V18A2,2 0 0,1 19,20H5C3.89,20 3,19.1 3,18V6A2,2 0 0,1 5,4H19M19,18V8H5V18H19Z"
|
||||
/>
|
||||
</svg>
|
||||
Rich User Interface
|
||||
</h2>
|
||||
<p>
|
||||
Use a beautiful and intuitive user interface to create, edit, and delete
|
||||
recipes. Recipe editor supports <b>markdown syntax</b>
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9,10V12H7V10H9M13,10V12H11V10H13M17,10V12H15V10H17M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5A2,2 0 0,1 5,3H6V1H8V3H16V1H18V3H19M19,19V8H5V19H19M9,14V16H7V14H9M13,14V16H11V14H13M17,14V16H15V14H17Z"
|
||||
/>
|
||||
</svg>
|
||||
Meal Planner
|
||||
</h2>
|
||||
<p>Create Meal Plans for the week, month, or year!</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Row 2 -->
|
||||
|
||||
<div class="top-hr">
|
||||
<div class="top-hr">
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M15.5,14L20.5,19L19,20.5L14,15.5V14.71L13.73,14.43C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.43,13.73L14.71,14H15.5M9.5,4.5L8.95,4.53C8.71,5.05 8.34,5.93 8.07,7H10.93C10.66,5.93 10.29,5.05 10.05,4.53C9.87,4.5 9.69,4.5 9.5,4.5M13.83,7C13.24,5.97 12.29,5.17 11.15,4.78C11.39,5.31 11.7,6.08 11.93,7H13.83M5.17,7H7.07C7.3,6.08 7.61,5.31 7.85,4.78C6.71,5.17 5.76,5.97 5.17,7M4.5,9.5C4.5,10 4.58,10.53 4.73,11H6.87L6.75,9.5L6.87,8H4.73C4.58,8.47 4.5,9 4.5,9.5M14.27,11C14.42,10.53 14.5,10 14.5,9.5C14.5,9 14.42,8.47 14.27,8H12.13C12.21,8.5 12.25,9 12.25,9.5C12.25,10 12.21,10.5 12.13,11H14.27M7.87,8L7.75,9.5L7.87,11H11.13C11.21,10.5 11.25,10 11.25,9.5C11.25,9 11.21,8.5 11.13,8H7.87M9.5,14.5C9.68,14.5 9.86,14.5 10.03,14.47C10.28,13.95 10.66,13.07 10.93,12H8.07C8.34,13.07 8.72,13.95 8.97,14.47L9.5,14.5M13.83,12H11.93C11.7,12.92 11.39,13.69 11.15,14.22C12.29,13.83 13.24,13.03 13.83,12M5.17,12C5.76,13.03 6.71,13.83 7.85,14.22C7.61,13.69 7.3,12.92 7.07,12H5.17Z" />
|
||||
</svg>
|
||||
Import Recipes
|
||||
</h2>
|
||||
<p>
|
||||
Quickly and easily import recipes from sites around the web using the built in <b>recipe scrapper</b>.
|
||||
</p>
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"
|
||||
/>
|
||||
</svg>
|
||||
Users
|
||||
</h2>
|
||||
<p>
|
||||
Add new users with sign-up links or simply create a new user in the
|
||||
admin panel.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,3A9,9 0 0,0 3,12H0L4,16L8,12H5A7,7 0 0,1 12,5A7,7 0 0,1 19,12A7,7 0 0,1 12,19C10.5,19 9.09,18.5 7.94,17.7L6.5,19.14C8.04,20.3 9.94,21 12,21A9,9 0 0,0 21,12A9,9 0 0,0 12,3M14,12A2,2 0 0,0 12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12Z" />
|
||||
</svg>
|
||||
Automatic Backups
|
||||
</h2>
|
||||
<p>
|
||||
Keep your data safe with automatic backups in any format supported by <b>Jinja2</b> templates
|
||||
</p>
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
|
||||
/>
|
||||
</svg>
|
||||
Groups
|
||||
</h2>
|
||||
<p>
|
||||
Sort users into groups to share recipes with the whole family, but keep
|
||||
your Meal Plans separate.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M19,4C20.11,4 21,4.9 21,6V18A2,2 0 0,1 19,20H5C3.89,20 3,19.1 3,18V6A2,2 0 0,1 5,4H19M19,18V8H5V18H19Z" />
|
||||
</svg>
|
||||
Rich User Interface
|
||||
</h2>
|
||||
<p> Use a beautiful and intuitive user interface to create, edit, and delete recipes. Recipe editor supports <b>markdown syntax</b> </p>
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M10.46,19C9,21.07 6.15,21.59 4.09,20.15C2.04,18.71 1.56,15.84 3,13.75C3.87,12.5 5.21,11.83 6.58,11.77L6.63,13.2C5.72,13.27 4.84,13.74 4.27,14.56C3.27,16 3.58,17.94 4.95,18.91C6.33,19.87 8.26,19.5 9.26,18.07C9.57,17.62 9.75,17.13 9.82,16.63V15.62L15.4,15.58L15.47,15.47C16,14.55 17.15,14.23 18.05,14.75C18.95,15.27 19.26,16.43 18.73,17.35C18.2,18.26 17.04,18.58 16.14,18.06C15.73,17.83 15.44,17.46 15.31,17.04L11.24,17.06C11.13,17.73 10.87,18.38 10.46,19M17.74,11.86C20.27,12.17 22.07,14.44 21.76,16.93C21.45,19.43 19.15,21.2 16.62,20.89C15.13,20.71 13.9,19.86 13.19,18.68L14.43,17.96C14.92,18.73 15.75,19.28 16.75,19.41C18.5,19.62 20.05,18.43 20.26,16.76C20.47,15.09 19.23,13.56 17.5,13.35C16.96,13.29 16.44,13.36 15.97,13.53L15.12,13.97L12.54,9.2H12.32C11.26,9.16 10.44,8.29 10.47,7.25C10.5,6.21 11.4,5.4 12.45,5.44C13.5,5.5 14.33,6.35 14.3,7.39C14.28,7.83 14.11,8.23 13.84,8.54L15.74,12.05C16.36,11.85 17.04,11.78 17.74,11.86M8.25,9.14C7.25,6.79 8.31,4.1 10.62,3.12C12.94,2.14 15.62,3.25 16.62,5.6C17.21,6.97 17.09,8.47 16.42,9.67L15.18,8.95C15.6,8.14 15.67,7.15 15.27,6.22C14.59,4.62 12.78,3.85 11.23,4.5C9.67,5.16 8.97,7 9.65,8.6C9.93,9.26 10.4,9.77 10.97,10.11L11.36,10.32L8.29,15.31C8.32,15.36 8.36,15.42 8.39,15.5C8.88,16.41 8.54,17.56 7.62,18.05C6.71,18.54 5.56,18.18 5.06,17.24C4.57,16.31 4.91,15.16 5.83,14.67C6.22,14.46 6.65,14.41 7.06,14.5L9.37,10.73C8.9,10.3 8.5,9.76 8.25,9.14Z"
|
||||
/>
|
||||
</svg>
|
||||
Webhooks
|
||||
</h2>
|
||||
<p>
|
||||
Schedule webhooks to send notifications to 3rd party services with
|
||||
todays Meal Plan data.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M9,10V12H7V10H9M13,10V12H11V10H13M17,10V12H15V10H17M19,3A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5A2,2 0 0,1 5,3H6V1H8V3H16V1H18V3H19M19,19V8H5V19H19M9,14V16H7V14H9M13,14V16H11V14H13M17,14V16H15V14H17Z" />
|
||||
</svg>
|
||||
Meal Planner
|
||||
</h2>
|
||||
<p>Create Meal Plans for the week, month, or year! </p>
|
||||
<h2>
|
||||
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7 7H5A2 2 0 0 0 3 9V17H5V13H7V17H9V9A2 2 0 0 0 7 7M7 11H5V9H7M14 7H10V17H12V13H14A2 2 0 0 0 16 11V9A2 2 0 0 0 14 7M14 11H12V9H14M20 9V15H21V17H17V15H18V9H17V7H21V9Z"
|
||||
/>
|
||||
</svg>
|
||||
Open API
|
||||
</h2>
|
||||
<p>
|
||||
<b>API Driven</b> application gives you full control of the backend
|
||||
server with interactive documentation
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Row 2 -->
|
||||
|
||||
<div class="top-hr">
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z" />
|
||||
</svg>
|
||||
Users
|
||||
</h2>
|
||||
<p>
|
||||
Add new users with sign-up links or simply create a new user in the admin panel.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z" />
|
||||
</svg>
|
||||
Groups
|
||||
</h2>
|
||||
<p>
|
||||
Sort users into groups to share recipes with the whole family, but keep your Meal Plans separate.
|
||||
</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M10.46,19C9,21.07 6.15,21.59 4.09,20.15C2.04,18.71 1.56,15.84 3,13.75C3.87,12.5 5.21,11.83 6.58,11.77L6.63,13.2C5.72,13.27 4.84,13.74 4.27,14.56C3.27,16 3.58,17.94 4.95,18.91C6.33,19.87 8.26,19.5 9.26,18.07C9.57,17.62 9.75,17.13 9.82,16.63V15.62L15.4,15.58L15.47,15.47C16,14.55 17.15,14.23 18.05,14.75C18.95,15.27 19.26,16.43 18.73,17.35C18.2,18.26 17.04,18.58 16.14,18.06C15.73,17.83 15.44,17.46 15.31,17.04L11.24,17.06C11.13,17.73 10.87,18.38 10.46,19M17.74,11.86C20.27,12.17 22.07,14.44 21.76,16.93C21.45,19.43 19.15,21.2 16.62,20.89C15.13,20.71 13.9,19.86 13.19,18.68L14.43,17.96C14.92,18.73 15.75,19.28 16.75,19.41C18.5,19.62 20.05,18.43 20.26,16.76C20.47,15.09 19.23,13.56 17.5,13.35C16.96,13.29 16.44,13.36 15.97,13.53L15.12,13.97L12.54,9.2H12.32C11.26,9.16 10.44,8.29 10.47,7.25C10.5,6.21 11.4,5.4 12.45,5.44C13.5,5.5 14.33,6.35 14.3,7.39C14.28,7.83 14.11,8.23 13.84,8.54L15.74,12.05C16.36,11.85 17.04,11.78 17.74,11.86M8.25,9.14C7.25,6.79 8.31,4.1 10.62,3.12C12.94,2.14 15.62,3.25 16.62,5.6C17.21,6.97 17.09,8.47 16.42,9.67L15.18,8.95C15.6,8.14 15.67,7.15 15.27,6.22C14.59,4.62 12.78,3.85 11.23,4.5C9.67,5.16 8.97,7 9.65,8.6C9.93,9.26 10.4,9.77 10.97,10.11L11.36,10.32L8.29,15.31C8.32,15.36 8.36,15.42 8.39,15.5C8.88,16.41 8.54,17.56 7.62,18.05C6.71,18.54 5.56,18.18 5.06,17.24C4.57,16.31 4.91,15.16 5.83,14.67C6.22,14.46 6.65,14.41 7.06,14.5L9.37,10.73C8.9,10.3 8.5,9.76 8.25,9.14Z" />
|
||||
</svg>
|
||||
Webhooks
|
||||
</h2>
|
||||
<p> Schedule webhooks to send notifications to 3rd party services with todays Meal Plan data. </p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<h2>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M7 7H5A2 2 0 0 0 3 9V17H5V13H7V17H9V9A2 2 0 0 0 7 7M7 11H5V9H7M14 7H10V17H12V13H14A2 2 0 0 0 16 11V9A2 2 0 0 0 14 7M14 11H12V9H14M20 9V15H21V17H17V15H18V9H17V7H21V9Z" />
|
||||
</svg>
|
||||
Open API
|
||||
</h2>
|
||||
<p> <b>API Driven</b> application gives you full control of the backend server with interactive documentation</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Custom narrow footer -->
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
<div class="md-footer-social">
|
||||
<a class="md-footer-social__link" href="https://github.com/hay-kot/mealie" rel="noopener" target="_blank" title="github.com">
|
||||
<svg viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg"><path d="M186.1 328.7c0 20.9-10.9 55.1-36.7 55.1s-36.7-34.2-36.7-55.1 10.9-55.1 36.7-55.1 36.7 34.2 36.7 55.1zM480 278.2c0 31.9-3.2 65.7-17.5 95-37.9 76.6-142.1 74.8-216.7 74.8-75.8 0-186.2 2.7-225.6-74.8-14.6-29-20.2-63.1-20.2-95 0-41.9 13.9-81.5 41.5-113.6-5.2-15.8-7.7-32.4-7.7-48.8 0-21.5 4.9-32.3 14.6-51.8 45.3 0 74.3 9 108.8 36 29-6.9 58.8-10 88.7-10 27 0 54.2 2.9 80.4 9.2 34-26.7 63-35.2 107.8-35.2 9.8 19.5 14.6 30.3 14.6 51.8 0 16.4-2.6 32.7-7.7 48.2 27.5 32.4 39 72.3 39 114.2zm-64.3 50.5c0-43.9-26.7-82.6-73.5-82.6-18.9 0-37 3.4-56 6-14.9 2.3-29.8 3.2-45.1 3.2-15.2 0-30.1-.9-45.1-3.2-18.7-2.6-37-6-56-6-46.8 0-73.5 38.7-73.5 82.6 0 87.8 80.4 101.3 150.4 101.3h48.2c70.3 0 150.6-13.4 150.6-101.3zm-82.6-55.1c-25.8 0-36.7 34.2-36.7 55.1s10.9 55.1 36.7 55.1 36.7-34.2 36.7-55.1-10.9-55.1-36.7-55.1z"></path></svg>
|
||||
<div class="md-footer-social">
|
||||
<a
|
||||
class="md-footer-social__link"
|
||||
href="https://github.com/hay-kot/mealie"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="github.com"
|
||||
>
|
||||
<svg viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M186.1 328.7c0 20.9-10.9 55.1-36.7 55.1s-36.7-34.2-36.7-55.1 10.9-55.1 36.7-55.1 36.7 34.2 36.7 55.1zM480 278.2c0 31.9-3.2 65.7-17.5 95-37.9 76.6-142.1 74.8-216.7 74.8-75.8 0-186.2 2.7-225.6-74.8-14.6-29-20.2-63.1-20.2-95 0-41.9 13.9-81.5 41.5-113.6-5.2-15.8-7.7-32.4-7.7-48.8 0-21.5 4.9-32.3 14.6-51.8 45.3 0 74.3 9 108.8 36 29-6.9 58.8-10 88.7-10 27 0 54.2 2.9 80.4 9.2 34-26.7 63-35.2 107.8-35.2 9.8 19.5 14.6 30.3 14.6 51.8 0 16.4-2.6 32.7-7.7 48.2 27.5 32.4 39 72.3 39 114.2zm-64.3 50.5c0-43.9-26.7-82.6-73.5-82.6-18.9 0-37 3.4-56 6-14.9 2.3-29.8 3.2-45.1 3.2-15.2 0-30.1-.9-45.1-3.2-18.7-2.6-37-6-56-6-46.8 0-73.5 38.7-73.5 82.6 0 87.8 80.4 101.3 150.4 101.3h48.2c70.3 0 150.6-13.4 150.6-101.3zm-82.6-55.1c-25.8 0-36.7 34.2-36.7 55.1s10.9 55.1 36.7 55.1 36.7-34.2 36.7-55.1-10.9-55.1-36.7-55.1z"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="md-footer-social__link" href="https://twitter.com/kot_hay" rel="noopener" target="_blank" title="twitter.com">
|
||||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path></svg>
|
||||
<a
|
||||
class="md-footer-social__link"
|
||||
href="https://twitter.com/kot_hay"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="twitter.com"
|
||||
>
|
||||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="md-footer-social__link" href="https://www.linkedin.com/in/hay-kot" rel="noopener" target="_blank" title="www.linkedin.com">
|
||||
<svg viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg"><path d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"></path></svg>
|
||||
<a
|
||||
class="md-footer-social__link"
|
||||
href="https://www.linkedin.com/in/hay-kot"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="www.linkedin.com"
|
||||
>
|
||||
<svg viewBox="0 0 448 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
{% block footer %}{% endblock %}
|
||||
{% endblock %} {% block content %}{% endblock %} {% block footer %}{% endblock
|
||||
%}
|
||||
|
|
|
@ -21,7 +21,13 @@
|
|||
<v-card-title> </v-card-title>
|
||||
<v-form @submit.prevent="select">
|
||||
<v-card-text>
|
||||
<v-text-field v-model="itemName" dense :label="inputLabel" :rules="[rules.required]"></v-text-field>
|
||||
<v-text-field
|
||||
v-model="itemName"
|
||||
dense
|
||||
:label="inputLabel"
|
||||
:rules="[rules.required]"
|
||||
autofocus
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<BaseButton cancel @click="dialog = false" />
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
@ -6,16 +8,17 @@ from jose import jwt
|
|||
from passlib.context import CryptContext
|
||||
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.db.data_access_layer.access_model_factory import Database
|
||||
from mealie.db.database import get_database
|
||||
from mealie.schema.user import PrivateUser
|
||||
|
||||
settings = get_app_settings()
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
|
||||
def create_access_token(data: dict(), expires_delta: timedelta = None) -> str:
|
||||
settings = get_app_settings()
|
||||
|
||||
to_encode = data.copy()
|
||||
expires_delta = expires_delta or timedelta(hours=settings.TOKEN_TIME)
|
||||
|
||||
|
@ -35,18 +38,61 @@ def create_recipe_slug_token(file_path: str) -> str:
|
|||
return create_access_token(token_data, expires_delta=timedelta(minutes=30))
|
||||
|
||||
|
||||
def authenticate_user(session, email: str, password: str) -> PrivateUser:
|
||||
db = get_database(session)
|
||||
def user_from_ldap(db: Database, session, username: str, password: str) -> PrivateUser:
|
||||
"""Given a username and password, tries to authenticate by BINDing to an
|
||||
LDAP server
|
||||
|
||||
If the BIND succeeds, it will either create a new user of that username on
|
||||
the server or return an existing one.
|
||||
Returns False on failure.
|
||||
"""
|
||||
import ldap
|
||||
|
||||
settings = get_app_settings()
|
||||
|
||||
conn = ldap.initialize(settings.LDAP_SERVER_URL)
|
||||
user_dn = settings.LDAP_BIND_TEMPLATE.format(username)
|
||||
try:
|
||||
conn.simple_bind_s(user_dn, password)
|
||||
except (ldap.INVALID_CREDENTIALS, ldap.NO_SUCH_OBJECT):
|
||||
return False
|
||||
|
||||
user = db.users.get_one(username, "username", any_case=True)
|
||||
if not user:
|
||||
user = db.users.create(
|
||||
{
|
||||
"username": username,
|
||||
"password": "LDAP",
|
||||
# Fill the next two values with something unique and vaguely
|
||||
# relevant
|
||||
"full_name": username,
|
||||
"email": username,
|
||||
"admin": False,
|
||||
},
|
||||
)
|
||||
|
||||
if settings.LDAP_ADMIN_FILTER:
|
||||
user.admin = len(conn.search_s(user_dn, ldap.SCOPE_BASE, settings.LDAP_ADMIN_FILTER, [])) > 0
|
||||
db.users.update(user.id, user)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def authenticate_user(session, email: str, password: str) -> PrivateUser | False:
|
||||
settings = get_app_settings()
|
||||
|
||||
db = get_database(session)
|
||||
user: PrivateUser = db.users.get(email, "email", any_case=True)
|
||||
|
||||
if not user:
|
||||
user = db.users.get(email, "username", any_case=True)
|
||||
if not user:
|
||||
|
||||
if settings.LDAP_AUTH_ENABLED and (not user or user.password == "LDAP"):
|
||||
return user_from_ldap(db, session, email, password)
|
||||
|
||||
if not user or not verify_password(password, user.password):
|
||||
return False
|
||||
|
||||
if not verify_password(password, user.password):
|
||||
return False
|
||||
return user
|
||||
|
||||
|
||||
|
|
|
@ -83,6 +83,25 @@ class AppSettings(BaseSettings):
|
|||
|
||||
return "" not in required and None not in required
|
||||
|
||||
# ===============================================
|
||||
# LDAP Configuration
|
||||
|
||||
LDAP_AUTH_ENABLED: bool = False
|
||||
LDAP_SERVER_URL: str = None
|
||||
LDAP_BIND_TEMPLATE: str = None
|
||||
LDAP_ADMIN_FILTER: str = None
|
||||
|
||||
@property
|
||||
def LDAP_ENABLED(self) -> bool:
|
||||
"""Validates LDAP settings are all set"""
|
||||
required = {
|
||||
self.LDAP_SERVER_URL,
|
||||
self.LDAP_BIND_TEMPLATE,
|
||||
self.LDAP_ADMIN_FILTER,
|
||||
}
|
||||
|
||||
return "" not in required and None not in required and self.LDAP_AUTH_ENABLED
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
|
|
@ -43,6 +43,8 @@ def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path:
|
|||
|
||||
def scrape_image(image_url: str, slug: str) -> Path:
|
||||
logger.info(f"Image URL: {image_url}")
|
||||
_FIREFOX_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0"
|
||||
|
||||
if isinstance(image_url, str): # Handles String Types
|
||||
pass
|
||||
|
||||
|
@ -54,7 +56,7 @@ def scrape_image(image_url: str, slug: str) -> Path:
|
|||
all_image_requests = []
|
||||
for url in image_url:
|
||||
try:
|
||||
r = requests.get(url, stream=True, headers={"User-Agent": ""})
|
||||
r = requests.get(url, stream=True, headers={"User-Agent": _FIREFOX_UA})
|
||||
except Exception:
|
||||
logger.exception("Image {url} could not be requested")
|
||||
continue
|
||||
|
@ -72,7 +74,7 @@ def scrape_image(image_url: str, slug: str) -> Path:
|
|||
filename = Recipe(slug=slug).image_dir.joinpath(filename)
|
||||
|
||||
try:
|
||||
r = requests.get(image_url, stream=True)
|
||||
r = requests.get(image_url, stream=True, headers={"User-Agent": _FIREFOX_UA})
|
||||
except Exception:
|
||||
logger.exception("Fatal Image Request Exception")
|
||||
return None
|
||||
|
|
43
poetry.lock
generated
43
poetry.lock
generated
|
@ -813,6 +813,17 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1-modules"
|
||||
version = "0.2.8"
|
||||
description = "A collection of ASN.1-based protocols modules."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
pyasn1 = ">=0.4.6,<0.5.0"
|
||||
|
||||
[[package]]
|
||||
name = "pycodestyle"
|
||||
version = "2.7.0"
|
||||
|
@ -1016,6 +1027,18 @@ cryptography = ["cryptography (>=3.4.0)"]
|
|||
pycrypto = ["pycrypto (>=2.6.0,<2.7.0)", "pyasn1"]
|
||||
pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"]
|
||||
|
||||
[[package]]
|
||||
name = "python-ldap"
|
||||
version = "3.3.1"
|
||||
description = "Python modules for implementing LDAP clients"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
||||
|
||||
[package.dependencies]
|
||||
pyasn1 = ">=0.3.7"
|
||||
pyasn1_modules = ">=0.1.5"
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.5"
|
||||
|
@ -1423,7 +1446,7 @@ pgsql = ["psycopg2-binary"]
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "597bcfac6b50f5f6e203db40e05546b1a9aaf4c8438790d233424cf66fc84d19"
|
||||
content-hash = "cd88ddf0b5bd0a771a2931c82acc8923fab2a743269e7ac0ae323eab9f1b38d5"
|
||||
|
||||
[metadata.files]
|
||||
aiofiles = [
|
||||
|
@ -2079,6 +2102,21 @@ pyasn1 = [
|
|||
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
|
||||
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
|
||||
]
|
||||
pyasn1-modules = [
|
||||
{file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"},
|
||||
{file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"},
|
||||
{file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"},
|
||||
{file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"},
|
||||
{file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"},
|
||||
{file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"},
|
||||
{file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"},
|
||||
]
|
||||
pycodestyle = [
|
||||
{file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
|
||||
{file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
|
||||
|
@ -2168,6 +2206,9 @@ python-jose = [
|
|||
{file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"},
|
||||
{file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"},
|
||||
]
|
||||
python-ldap = [
|
||||
{file = "python-ldap-3.3.1.tar.gz", hash = "sha256:4711cacf013e298754abd70058ccc995758177fb425f1c2d30e71adfc1d00aa5"},
|
||||
]
|
||||
python-multipart = [
|
||||
{file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"},
|
||||
]
|
||||
|
|
|
@ -37,6 +37,7 @@ psycopg2-binary = {version = "^2.9.1", optional = true}
|
|||
gunicorn = "^20.1.0"
|
||||
emails = "^0.6"
|
||||
python-i18n = "^0.3.9"
|
||||
python-ldap = "^3.3.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pylint = "^2.6.0"
|
||||
|
|
|
@ -34,3 +34,9 @@ LANG=en-US
|
|||
# SMTP_USER=""
|
||||
# SMTP_PASSWORD=""
|
||||
|
||||
# Configuration for authentication via an external LDAP server
|
||||
LDAP_AUTH_ENABLED=False
|
||||
LDAP_SERVER_URL=None
|
||||
LDAP_BIND_TEMPLATE=None
|
||||
LDAP_ADMIN_FILTER=None
|
||||
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
from pathlib import Path
|
||||
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
from mealie.core import security
|
||||
from mealie.core.config import get_app_settings
|
||||
from mealie.core.dependencies import validate_file_token
|
||||
from mealie.db.db_setup import create_session
|
||||
from tests.utils.factories import random_string
|
||||
|
||||
|
||||
def test_create_file_token():
|
||||
|
@ -9,3 +14,39 @@ def test_create_file_token():
|
|||
file_token = security.create_file_token(file_path)
|
||||
|
||||
assert file_path == validate_file_token(file_token)
|
||||
|
||||
|
||||
def test_ldap_authentication_mocked(monkeypatch: MonkeyPatch):
|
||||
import ldap
|
||||
|
||||
user = random_string(10)
|
||||
password = random_string(10)
|
||||
bind_template = "cn={},dc=example,dc=com"
|
||||
admin_filter = "(memberOf=cn=admins,dc=example,dc=com)"
|
||||
monkeypatch.setenv("LDAP_AUTH_ENABLED", "true")
|
||||
monkeypatch.setenv("LDAP_SERVER_URL", "") # Not needed due to mocking
|
||||
monkeypatch.setenv("LDAP_BIND_TEMPLATE", bind_template)
|
||||
monkeypatch.setenv("LDAP_ADMIN_FILTER", admin_filter)
|
||||
|
||||
class LdapConnMock:
|
||||
def simple_bind_s(self, dn, bind_pw):
|
||||
assert dn == bind_template.format(user)
|
||||
return bind_pw == password
|
||||
|
||||
def search_s(self, dn, scope, filter, attrlist):
|
||||
assert attrlist == []
|
||||
assert filter == admin_filter
|
||||
assert dn == bind_template.format(user)
|
||||
assert scope == ldap.SCOPE_BASE
|
||||
return [()]
|
||||
|
||||
def ldap_initialize_mock(url):
|
||||
assert url == ""
|
||||
return LdapConnMock()
|
||||
|
||||
monkeypatch.setattr(ldap, "initialize", ldap_initialize_mock)
|
||||
|
||||
get_app_settings.cache_clear()
|
||||
result = security.authenticate_user(create_session(), user, password)
|
||||
assert result is not False
|
||||
assert result.username == user
|
||||
|
|
Loading…
Reference in a new issue