Compare commits

..

12 Commits

Author SHA1 Message Date
Hayden
8154ee548a Chore/bump deps (#853)
* add LDAP dep

* bump extruct and scraper

* allow for disabling auto backup

* v0.5.4 changelog
2021-12-01 21:07:17 -09:00
Hayden
591bf9d98f Update v1-task.yaml 2021-11-24 11:46:43 -09:00
Hayden
f202b1f922 Update v1-task.yaml 2021-11-24 11:45:59 -09:00
Hayden
17b2e37e4e Update v1-task.yaml 2021-11-24 11:40:53 -09:00
Hayden
0ec8087ac6 Rename v1-task to v1-task.yaml 2021-11-24 11:33:59 -09:00
Hayden
e580d6f904 Create v1-tasks template 2021-11-24 11:33:26 -09:00
dvdkon
56d9cafb68 Add LDAP authentication support (v2, onto dev) (#803)
* Add LDAP authentication support

* Add test for LDAP authentication
2021-11-24 08:59:03 -09:00
Hayden
32c864c703 New Crowdin updates (#818)
* New translations en-US.json (French, Canada)

* New translations en-US.json (French, Canada)

* New translations en-US.json (French, Canada)

* New translations en-US.json (French)

* New translations en-US.json (French, Canada)

* New translations en-US.json (French, Canada)

* New translations en-US.json (French)
2021-11-23 20:38:25 -09:00
Bryce Willey
37280a3da0 Improve the SWAG Community Guide (#793)
* Freshen up the SWAG documentation

Added some extra pointers for setting up HTTPS with Mealie and SWAG, and indented the `docker-compose.yml` as it should be.

* Replaced <code> html blocks with backticks

* Better formatting and comments in example config files

* Made DuckDNS consistent with other places on the page
2021-11-23 20:34:23 -09:00
cadamswaite
7f850fba98 Use firefox UA when making requests (#780) 2021-11-23 20:34:10 -09:00
J.P. Krauss
b40f201430 Allow arrow keys to function when SearchDialog is not open (#777) 2021-11-07 10:11:07 -09:00
sephrat
a0d796551c Add support for new languages (#781)
* Add support for Slovak language

* Add support for new languages

Catalan, Danish, Norwegian and Russian
2021-11-07 10:03:37 -09:00
20 changed files with 1361 additions and 505 deletions

38
.github/ISSUE_TEMPLATE/v1-task.yaml vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: v1.0.0b Task
description: "CONTRIBUTORS ONLY: Submit a Task that needs to be completed"
title: "[v1.0.0b] [Task] - TASK DESCRIPTION"
labels:
- task
- v1
body:
- type: markdown
attributes:
value: |
Thanks for your interest in Mealie! 🚀
This is a place for Mealie contributors to find tasks that need to get done around the repository. Tasks are different than issues as they are generally related to providing a new feature or improve an existing feature. They are _generally_ not related to an issue.
**DO NOT** create a task unless
- You are a contributors who has prior approval via discord/discussions
- You have otherwise been given approval to post the tasks
Otherwise, your post will be closed/deleted.
**Interested in Taking This?**
If you're interested in completing this tasks and it hasn't already been taken, comment below and to let others know you're working on it. As you work through the task, I ask that you submit a draft pull request as soon as possible, and tag this issue so we can all collaborate as best as possible.
- type: textarea
id: problem
attributes:
label: What is the problem this task addresses?
placeholder: A clear and concise description of what the problem this task will address.
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed/Possible Solution(s)?
placeholder: Provide as much context around the idea as possible with potential files and roadblocks that may come up
validations:
required: true

View File

@@ -57,6 +57,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'

View File

@@ -44,6 +44,8 @@ RUN apt-get update \
build-essential \
libpq-dev \
libwebp-dev \
# LDAP Dependencies
libsasl2-dev libldap2-dev libssl-dev \
gnupg gnupg2 gnupg1 \
debian-keyring \
debian-archive-keyring \

View File

@@ -0,0 +1,28 @@
# v0.5.4 - Bug Fixes
**App Version: v0.5.4**
**Database Version: v0.5.0**
## Breaking Changes
!!! error "Breaking Changes"
None
## What's Changed
* Add support for new languages by @sephrat in https://github.com/hay-kot/mealie/pull/781
* Allow arrow keys to function when SearchDialog is not open by @asymworks in https://github.com/hay-kot/mealie/pull/777
* Use firefox user agent when making requests by @cadamswaite in https://github.com/hay-kot/mealie/pull/780
* Improve the SWAG Community Guide by @BryceStevenWilley in https://github.com/hay-kot/mealie/pull/793
* New Crowdin updates by @hay-kot in https://github.com/hay-kot/mealie/pull/818
* Add LDAP authentication support (v2, onto dev) by @dvdkon in https://github.com/hay-kot/mealie/pull/803
* Auto backup is now disabled by default. Enable it by setting the AUTO_BACKUP_ENABLED env variable to true.
## New Contributors
* @asymworks made their first contribution in https://github.com/hay-kot/mealie/pull/777
* @dvdkon made their first contribution in https://github.com/hay-kot/mealie/pull/803
**Full Changelog**: https://github.com/hay-kot/mealie/compare/v0.5.3...v0.5.4

View File

@@ -4,89 +4,96 @@
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
To make the setup of a Reverse Proxy much easier, Linuxserver.io developed [SWAG](https://github.com/linuxserver/docker-swag)
SWAG - Secure Web Application Gateway (formerly known as letsencrypt, no relation to Let's Encrypt™) sets up an Nginx web server and reverse proxy with PHP support and a built-in certbot client that automates free SSL server certificate generation and renewal processes (Let's Encrypt and ZeroSSL). It also contains fail2ban for intrusion prevention.
SWAG - Secure Web Application Gateway (formerly known as letsencrypt, no relation to Let's Encrypt™) sets up an Nginx web server and reverse proxy with PHP support and a built-in certbot client that automates free TLS server certificate generation and renewal processes (Let's Encrypt and ZeroSSL). It also contains fail2ban for intrusion prevention.
## Step 1: Get a domain
The first step is to grab a dynamic DNS if you don't have your own subdomain already. You can get this from for example [DuckDNS](https://www.duckdns.org).
If you already own a domain, you'll need to create an `A` record that points to the machine that SWAG is running on. See
the [SWAG documentation](https://docs.linuxserver.io/general/swag#create-container-via-http-validation) for more details.
## Step 2: Set-up SWAG
Then you will need to set up SWAG, the variables of the docker-compose are explained on the Github page of [SWAG](https://github.com/linuxserver/docker-swag).
This is an example of how to set it up using duckdns and docker-compose.
This is an example of how to set it up using DuckDNS and docker-compose.
!!! example "docker-compose.yml"
```yaml
version: "2.1"
services:
swag:
image: ghcr.io/linuxserver/swag
container_name: swag
cap_add:
- NET_ADMIN
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Brussels
- URL=<mydomain.duckdns>
- SUBDOMAINS=wildcard
- VALIDATION=duckdns
- CERTPROVIDER= #optional
- DNSPLUGIN= #optional
- DUCKDNSTOKEN=<duckdnstoken>
- EMAIL=<e-mail> #optional
- ONLY_SUBDOMAINS=false #optional
- EXTRA_DOMAINS=<extradomains> #optional
- STAGING=false #optional
volumes:
- /etc/config/swag:/config
ports:
- 443:443
restart: unless-stopped
swag:
image: ghcr.io/linuxserver/swag
container_name: swag
cap_add:
- NET_ADMIN
environment:
- PUID=1000
- PGID=1000
# valid TZs at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
- TZ=Europe/Brussels
- URL=<mydomain.duckdns>
- SUBDOMAINS=wildcard
- VALIDATION=duckdns
- CERTPROVIDER= #optional
- DNSPLUGIN= #optional
- DUCKDNSTOKEN=<duckdnstoken>
- EMAIL=<e-mail> #optional
- ONLY_SUBDOMAINS=false #optional
- EXTRA_DOMAINS=<extradomains> #optional
- STAGING=false #optional
volumes:
- /etc/config/swag:/config
ports:
- 443:443
# required if VALIDATION=http above, if you aren't using DuckDNS
- 80:80
restart: unless-stopped
```
Don't forget to change the <code>mydomain.duckns</code> into your personal domain and the <code>duckdnstoken</code> into your token and remove the brackets.
Don't forget to change the `mydomain.duckns` into your personal domain and the `duckdnstoken` into your token and remove the brackets.
You can also include the contents of the [mealie docker-compose](mealie/documentation/getting-started/install/#docker-compose-with-sqlite) in the SWAG
docker-compose, without the `ports` section under mealie. This allows SWAG and mealie to communicate on the same docker network, without
making mealie visible to other applications on your machine.
## Step 3: Change the config files
Navigate to the config folder of SWAG and head to <code>proxy-confs</code>. If you used the example above, you should navigate to: <code>/etc/config/swag/nginx/proxy-confs/</code>.
Navigate to the config folder of SWAG and head to `proxy-confs`. If you used the example above, you should navigate to: `/etc/config/swag/nginx/proxy-confs/`.
There are a lot of preconfigured files to use for different apps such as radarr, sonarr, overseerr, ...
To use the bundled configuration file, simply rename <code>mealie.subdomain.conf.sample</code> in the proxy-confs folder to <code>mealie.subdomain.conf</code>.
Alternatively, you can create a new file <code>mealie.subdomain.conf</code> in proxy-confs with the following configuration:
To use the bundled configuration file, simply rename `mealie.subdomain.conf.sample` in the proxy-confs folder to `mealie.subdomain.conf`.
Alternatively, you can create a new file `mealie.subdomain.conf` in proxy-confs with the following configuration:
!!! example "mealie.subdomain.conf"
```yaml
server {
```nginx
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name mealie.*;
server_name mealie.*;
include /config/nginx/ssl.conf;
include /config/nginx/ssl.conf;
client_max_body_size 0;
client_max_body_size 0;
location / {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app mealie;
set $upstream_port 80;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
location / {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app mealie;
set $upstream_port 80;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
## Step 4: Port-forward port 443
Since SWAG allows you to set up a secure connection, you will need to open port 443 on your router for encrypted traffic. This is way more secure than port 80 for http.
Since SWAG allows you to set up a secure connection, you will need to open port 443 on your router for encrypted traffic. This is way more secure than port 80 for http. For more information about using TLS on port 443, see [SWAG's documentation](https://docs.linuxserver.io/general/swag#cert-provider-lets-encrypt-vs-zerossl) on cert providers and port forwarding.
## Step 5: Restart SWAG
When you change anything in the config of Nginx, you will need to restart the container using <code>docker restart swag</code>.
If everything went well, you can now access mealie on the subdomain you configured: mealie.mydomain.duckdns.org
When you change anything in the config of Nginx, you will need to restart the container using `docker restart swag`.
If everything went well, you can now access mealie on the subdomain you configured: `mealie.mydomain.duckdns.org`

View File

@@ -128,12 +128,17 @@ services:
| POSTGRES_PORT | 5432 | Postgres database port |
| POSTGRES_DB | mealie | Postgres database name |
| TOKEN_TIME | 2 | The time in hours that a login/auth token is valid |
| 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)`) |
| RECIPE_PUBLIC | True | Default Recipe Settings - Make Recipe Public |
| RECIPE_SHOW_NUTRITION | True | Default Recipe Settings - Show Recipe Nutrition |
| RECIPE_SHOW_ASSETS | True | Default Recipe Settings - Show Recipe Assets |
| RECIPE_LANDSCAPE_VIEW | True | Default Recipe Settings - Set Landscape View |
| RECIPE_DISABLE_COMMENTS | False | Default Recipe Settings - Disable Comments |
| RECIPE_DISABLE_AMOUNT | False | Default Recipe Settings - Disable Amount |
| AUTO_BACKUP_ENABLED | False | Disable/Enable Mealie's Auto Backup Function |
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
| API_DOCS | True | Turns on/off access to the API documentation locally. |
| TZ | UTC | Must be set to get correct date/time on the server |

View File

@@ -136,16 +136,18 @@ export default {
this.$emit(SELECTED_EVENT, recipe);
},
onUpDown(e) {
if (e.keyCode === 38) {
e.preventDefault();
this.selectedIndex--;
} else if (e.keyCode === 40) {
e.preventDefault();
this.selectedIndex++;
} else {
return;
if (this.dialog) {
if (e.keyCode === 38) {
e.preventDefault();
this.selectedIndex--;
} else if (e.keyCode === 40) {
e.preventDefault();
this.selectedIndex++;
} else {
return;
}
this.selectRecipe();
}
this.selectRecipe();
},
resetSelected() {
this.searchString = "";
@@ -169,4 +171,4 @@ export default {
</script>
<style >
</style>
</style>

View File

@@ -0,0 +1,21 @@
{
"short": {
"month": "short",
"day": "numeric",
"weekday": "long"
},
"medium": {
"month": "long",
"day": "numeric",
"weekday": "long",
"year": "numeric"
},
"long": {
"year": "numeric",
"month": "long",
"day": "numeric",
"weekday": "long",
"hour": "numeric",
"minute": "numeric"
}
}

View File

@@ -0,0 +1,489 @@
{
"about": {
"about": "À propos",
"about-mealie": "À propos de Mealie",
"api-docs": "Documentation de l'API",
"api-port": "Port de l'API",
"application-mode": "Mode de l'application",
"database-type": "Type de base de données",
"database-url": "URL de la base de données",
"default-group": "Groupe par défaut",
"demo": "Oui",
"demo-status": "Mode démo",
"development": "Développement",
"docs": "Documentation",
"download-log": "Télécharger les logs",
"download-recipe-json": "Dernier JSON récupéré",
"github": "GitHub",
"log-lines": "Lignes de log",
"not-demo": "Non",
"portfolio": "Portfolio",
"production": "Production",
"support": "Soutenir",
"version": "Version"
},
"asset": {
"assets": "Ressources",
"code": "Code",
"file": "Fichier",
"image": "Image",
"new-asset": "Nouvelle ressource",
"pdf": "PDF",
"recipe": "Recette",
"show-assets": "Afficher les ressources"
},
"category": {
"category-created": "Catégorie créée",
"category-creation-failed": "La création de la catégorie a échoué",
"category-deleted": "Catégorie supprimée",
"category-deletion-failed": "La suppression de la catégorie a échoué",
"category-filter": "Filtre par catégories",
"category-update-failed": "La mise à jour de la catégorie a échoué",
"category-updated": "Catégorie mise à jour",
"uncategorized-count": "{count} non catégorisée|{count} non catégorisées"
},
"events": {
"apprise-url": "URL apprise",
"database": "Base de données",
"delete-event": "Supprimer lévènement",
"new-notification-form-description": "Mealie utilise la bibliothèque apprise pour générer des notifications. Elle propose de nombreux services à utiliser pour les notifications. Consultez leur wiki pour un guide complet sur la façon de créer l'URL de votre service. Si disponible, sélectionner le type de votre notification peut inclure des fonctionnalités supplémentaires.",
"new-version": "Nouvelle version disponible !",
"notification": "Notification",
"refresh": "Rafraîchir",
"scheduled": "Planifié",
"something-went-wrong": "Une erreur s'est produite !",
"subscribed-events": "Évènements suivis",
"test-message-sent": "Message de test envoyé"
},
"general": {
"cancel": "Annuler",
"clear": "Effacer",
"close": "Fermer",
"confirm": "Confirmer",
"confirm-delete-generic": "Voulez-vous vraiment supprimer ceci ?",
"copied": "Copié",
"create": "Créer",
"created": "Créé",
"custom": "Personnalisé",
"dashboard": "Tableau de bord",
"delete": "Supprimer",
"disabled": "Désactivé",
"download": "Télécharger",
"edit": "Modifier",
"enabled": "Activé",
"exception": "Exception",
"failed-count": "Échec : {count}",
"failure-uploading-file": "Échec de l'envoi du fichier",
"favorites": "Favoris",
"field-required": "Champ obligatoire",
"file-folder-not-found": "Fichier/dossier introuvable",
"file-uploaded": "Fichier envoyé",
"filter": "Filtrer",
"friday": "Vendredi",
"general": "Général",
"get": "Envoyer",
"home": "Page d'accueil",
"image": "Image",
"image-upload-failed": "Le téléchargement de l'image a échoué",
"import": "Importer",
"json": "JSON",
"keyword": "Mot-clé",
"link-copied": "Lien copié",
"loading-recipes": "Chargement des recettes",
"monday": "Lundi",
"name": "Nom",
"new": "Nouveau",
"no": "Non",
"no-recipe-found": "Aucune recette trouvée",
"ok": "OK",
"options": "Options :",
"print": "Imprimer",
"random": "Aléatoire",
"rating": "Note",
"recent": "Récent",
"recipe": "Recette",
"recipes": "Recettes",
"rename-object": "Renommer {0}",
"reset": "Réinitialiser",
"saturday": "Samedi",
"save": "Sauvegarder",
"settings": "Paramètres",
"share": "Partager",
"shuffle": "Aléatoire",
"sort": "Trier",
"sort-alphabetically": "Alphabétique",
"status": "Statut",
"submit": "Importer",
"success-count": "Succès : {count}",
"sunday": "Dimanche",
"templates": "Modèles :",
"test": "Tester",
"themes": "Thèmes",
"thursday": "Jeudi",
"token": "Jeton",
"tuesday": "Mardi",
"type": "Type",
"update": "Mettre à jour",
"updated": "Mis à jour",
"upload": "Importer",
"url": "URL",
"view": "Afficher",
"wednesday": "Mercredi",
"yes": "Oui"
},
"group": {
"are-you-sure-you-want-to-delete-the-group": "Êtes-vous certain de vouloir supprimer <b>{groupName}<b/> ?",
"cannot-delete-default-group": "Vous ne pouvez pas supprimer le groupe par défaut",
"cannot-delete-group-with-users": "Impossible de supprimer un groupe avec des utilisateurs",
"confirm-group-deletion": "Confirmer la suppression du groupe",
"create-group": "Créer un groupe",
"error-updating-group": "Erreur lors de la mise à jour du groupe",
"group": "Groupe",
"group-deleted": "Groupe supprimé",
"group-deletion-failed": "Échec de la suppression du groupe",
"group-id-with-value": "ID groupe : {groupID}",
"group-name": "Nom du groupe",
"group-not-found": "Groupe non trouvé",
"group-with-value": "Groupe : {groupID}",
"groups": "Groupes",
"manage-groups": "Gérer les groupes",
"user-group": "Groupe d'utilisateurs",
"user-group-created": "Groupe d'utilisateurs créé",
"user-group-creation-failed": "La création du groupe d'utilisateur a échoué"
},
"meal-plan": {
"create-a-new-meal-plan": "Créer un nouveau menu",
"dinner-this-week": "Menu de la semaine",
"dinner-today": "Menu du jour",
"dinner-tonight": "AU MENU CE SOIR",
"edit-meal-plan": "Modifier le menu",
"end-date": "Date de fin",
"group": "Regrouper (Bêta)",
"main": "Plat principal",
"meal-planner": "Menus",
"meal-plans": "Menus",
"mealplan-categories": "CATÉGORIES DES MENUS",
"mealplan-created": "Menu créé",
"mealplan-creation-failed": "La création du menu a échoué",
"mealplan-deleted": "Menu supprimé",
"mealplan-deletion-failed": "La suppression du menu a échoué",
"mealplan-settings": "Paramètres des menus",
"mealplan-update-failed": "La mise à jour du menu a échoué",
"mealplan-updated": "Menu mis à jour",
"no-meal-plan-defined-yet": "Aucun menu planifié",
"no-meal-planned-for-today": "Aucun repas prévu pour aujourd'hui",
"only-recipes-with-these-categories-will-be-used-in-meal-plans": "Seules les recettes appartenant à ces catégories seront utilisées dans les menus",
"planner": "Planificateur",
"quick-week": "Semaine rapide",
"side": "Accompagnement",
"sides": "Accompagnements",
"start-date": "Date de début"
},
"migration": {
"chowdown": {
"description": "Importer des recettes depuis Chowdown",
"title": "Chowdown"
},
"migration-data-removed": "Données de migration supprimées",
"nextcloud": {
"description": "Importer des recettes depuis un livre de recettes Nextcloud existant",
"title": "Nextcloud Cookbook"
},
"no-migration-data-available": "Aucune donnée d'importation n'est disponible",
"recipe-migration": "Migrer les recettes"
},
"new-recipe": {
"bulk-add": "Ajouter en masse",
"error-details": "Seuls les sites web contenant ld+json ou des microdonnées peuvent être importés par Mealie. La plupart des grands sites web de recettes sont compatibles avec cette structure de données. Si votre site ne peut pas être importé mais qu'il y a des données json dans le journal, veuillez soumettre un problème GitHub avec l'URL et les données.",
"error-title": "On dirait qu'on n'a pas pu trouver quoi que ce soit",
"from-url": "Depuis une adresse web",
"github-issues": "Anomalies GitHub",
"google-ld-json-info": "Infos Json-Ld Google",
"must-be-a-valid-url": "Doit être une URL valide",
"paste-in-your-recipe-data-each-line-will-be-treated-as-an-item-in-a-list": "Copiez votre recette ici. Chaque ligne sera traitée comme un objet de la liste",
"recipe-markup-specification": "Spécification du marquage des recettes",
"recipe-url": "Adresse de la recette",
"upload-a-recipe": "Télécharger une recette",
"upload-individual-zip-file": "Chargez un fichier .zip exporté depuis une autre instance Mealie.",
"url-form-hint": "Copiez et collez un lien depuis votre site de recettes favori",
"view-scraped-data": "Voir les données récupérées"
},
"page": {
"404-page-not-found": "404 Page introuvable",
"all-recipes": "Toutes les recettes",
"new-page-created": "Nouvelle page créée",
"page": "Page",
"page-creation-failed": "La création de la page a échoué",
"page-deleted": "Page supprimée",
"page-deletion-failed": "La suppression de la page a échoué",
"page-update-failed": "La mise à jour de la page a échoué",
"page-updated": "Page mise à jour",
"pages-update-failed": "La mise à jour des pages a échoué",
"pages-updated": "Pages mises à jour"
},
"recipe": {
"add-key": "Ajouter une clé",
"add-to-favorites": "Ajouter aux favoris",
"api-extras": "Extras API",
"calories": "Calories",
"calories-suffix": "calories",
"carbohydrate-content": "Glucides",
"categories": "Catégories",
"comment-action": "Commenter",
"comments": "Commentaires",
"delete-confirmation": "Êtes-vous sûr(e) de vouloir supprimer cette recette ?",
"delete-recipe": "Supprimer la recette",
"description": "Description",
"disable-amount": "Désactiver les quantités d'ingrédients",
"disable-comments": "Désactiver les commentaires",
"fat-content": "Matières grasses",
"fiber-content": "Fibres",
"grams": "grammes",
"ingredient": "Ingrédient",
"ingredients": "Ingrédients",
"insert-section": "Insérer une section",
"instructions": "Instructions",
"key-name-required": "Un nom de clé est requis",
"landscape-view-coming-soon": "Vue paysage (bientôt disponible)",
"milligrams": "milligrammes",
"new-key-name": "Nouveau nom de clé",
"no-white-space-allowed": "Aucun espace blanc autorisé",
"note": "Note",
"nutrition": "Valeurs nutritionnelles",
"object-key": "Clé d'objet",
"object-value": "Valeur d'objet",
"original-url": "Recette originale",
"perform-time": "Temps de cuisson",
"prep-time": "Temps de préparation",
"protein-content": "Protéines",
"public-recipe": "Recette publique",
"recipe-created": "Recette créée",
"recipe-creation-failed": "La création de la recette a échoué",
"recipe-deleted": "Recette supprimée",
"recipe-image": "Image de la recette",
"recipe-image-updated": "L'image de la recette a été mise à jour",
"recipe-name": "Nom de la recette",
"recipe-settings": "Paramètres de la recette",
"recipe-update-failed": "La mise à jour de la recette a échoué",
"recipe-updated": "Recette mise à jour",
"remove-from-favorites": "Supprimer des favoris",
"remove-section": "Supprimer une section",
"save-recipe-before-use": "Enregistrez la recette avant utilisation",
"section-title": "Titre de la section",
"servings": "Portions",
"share-recipe-message": "Je voulais partager ma recette de {0} avec vous.",
"show-nutrition-values": "Afficher les valeurs nutritionnelles",
"sodium-content": "Sodium",
"step-index": "Étape {step}",
"sugar-content": "Sucres",
"title": "Titre",
"total-time": "Temps total",
"unable-to-delete-recipe": "Impossible de supprimer la recette"
},
"reicpe": {
"no-recipe": "Pas de recette"
},
"search": {
"advanced-search": "Recherche avancée",
"and": "et",
"exclude": "Exclure",
"include": "Inclure",
"max-results": "Nombre de résultats maximum",
"or": "Ou",
"results": "Résultats",
"search": "Rechercher",
"search-mealie": "Rechercher dans Mealie (appuyez sur /)",
"search-placeholder": "Rechercher...",
"tag-filter": "Filtre par mots-clés"
},
"settings": {
"add-a-new-theme": "Ajouter un nouveau thème",
"admin-settings": "Paramètres d'administration",
"backup": {
"backup-created-at-response-export_path": "Sauvegarde créée dans {path}",
"backup-deleted": "Sauvegarde supprimée",
"backup-tag": "Tag de la sauvegarde",
"create-heading": "Créer une sauvegarde",
"delete-backup": "Supprimer la sauvegarde",
"error-creating-backup-see-log-file": "Erreur de création de la sauvegarde. Voir les logs",
"full-backup": "Sauvegarde complète",
"import-summary": "Résumé de l'importation",
"partial-backup": "Sauvegarde partielle",
"unable-to-delete-backup": "Impossible de supprimer la sauvegarde."
},
"backup-and-exports": "Sauvegardes",
"change-password": "Modifier le mot de passe",
"current": "Version :",
"custom-pages": "Pages personnalisées",
"edit-page": "Modifier la page",
"events": "Évènements",
"first-day-of-week": "Premier jour de la semaine",
"group-settings-updated": "Paramètres du groupe mis à jour",
"homepage": {
"all-categories": "Toutes les catégories",
"card-per-section": "Tuiles par section",
"home-page": "Page d'accueil",
"home-page-sections": "Sections de la page d'accueil",
"show-recent": "Afficher les récentes"
},
"language": "Langue",
"latest": "Dernière",
"local-api": "API locale",
"locale-settings": "Paramètres régionaux",
"migrations": "Migrations",
"new-page": "Nouvelle page",
"notify": "Notifier",
"organize": "Organiser",
"page-name": "Nom de la page",
"pages": "Pages",
"profile": "Profil",
"remove-existing-entries-matching-imported-entries": "Supprimer les entrées existantes correspondant aux entrées importées",
"set-new-time": "Indiquer une nouvelle heure",
"settings-update-failed": "La mise à jour des paramètres a échoué",
"settings-updated": "Paramètres mis à jour",
"site-settings": "Paramètres du site",
"theme": {
"accent": "Accentué",
"dark": "Sombre",
"default-to-system": "Identique au système",
"error": "Erreur",
"error-creating-theme-see-log-file": "Erreur lors de la création du thème. Consultez les logs.",
"error-deleting-theme": "Erreur lors de la suppression du thème",
"error-updating-theme": "Erreur lors de la mise à jour du thème",
"info": "Information",
"light": "Clair",
"primary": "Primaire",
"secondary": "Secondaire",
"success": "Succès",
"switch-to-dark-mode": "Basculer en mode sombre",
"switch-to-light-mode": "Basculer en mode clair",
"theme-deleted": "Thème supprimé",
"theme-name": "Nom du thème",
"theme-name-is-required": "Un nom de thème est requis.",
"theme-saved": "Thème enregistré",
"theme-updated": "Thème mis à jour",
"warning": "Avertissement"
},
"token": {
"active-tokens": "JETONS ACTIFS",
"api-token": "Jeton de l'API",
"api-tokens": "Jetons de l'API",
"copy-this-token-for-use-with-an-external-application-this-token-will-not-be-viewable-again": "Copiez ce jeton pour l'utiliser avec une application externe. Ce jeton ne sera plus consultable.",
"create-an-api-token": "Créer un jeton API",
"token-name": "Nom du jeton"
},
"toolbox": {
"assign-all": "Assigner tout",
"bulk-assign": "Assigner en masse",
"new-name": "Nouveau nom",
"no-unused-items": "Aucun élément inutilisé",
"recipes-affected": "Aucune recette affectée|Une recette affectée|{count} recettes affectées",
"remove-unused": "Supprimer orphelins",
"title-case-all": "Majuscules partout",
"toolbox": "Boîte à outils",
"unorganized": "Non organisé(s)"
},
"webhooks": {
"test-webhooks": "Tester les webhooks",
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "Les liens dans cette liste recevront les webhooks contenant les recettes pour le menu du jour. Actuellement, les webhooks se lancent à",
"webhook-url": "Lien du webhook",
"webhooks-caps": "WEBHOOKS"
}
},
"shopping-list": {
"all-lists": "Toutes les listes",
"create-shopping-list": "Créer une liste d'épicerie",
"from-recipe": "À partir d'une recette",
"list-name": "Nom de la liste",
"new-list": "Nouvelle liste",
"quantity": "Quantité : {0}",
"shopping-list": "Liste d'épicerie",
"shopping-lists": "Listes d'épicerie"
},
"sidebar": {
"all-recipes": "Les recettes",
"categories": "Catégories",
"dashboard": "Dashboard",
"home-page": "Accueil",
"manage-users": "Utilisateurs",
"migrations": "Migrations",
"profile": "Profil",
"search": "Rechercher",
"site-settings": "Paramètres",
"tags": "Mots-clés",
"toolbox": "Boîte à outils"
},
"signup": {
"error-signing-up": "Erreur lors de l'inscription",
"sign-up": "S'inscrire",
"sign-up-link-created": "Lien d'inscription créé",
"sign-up-link-creation-failed": "La création du lien d'inscription a échoué",
"sign-up-links": "Liens d'inscription",
"sign-up-token-deleted": "Jeton d'inscription supprimé",
"sign-up-token-deletion-failed": "La suppression du jeton d'inscription a échoué",
"welcome-to-mealie": "Bienvenue sur Mealie ! Pour devenir un utilisateur de cette instance, vous devez avoir un lien d'invitation valide. Si vous n'avez pas reçu d'invitation, vous ne pouvez pas vous inscrire. Pour recevoir un lien, contactez l'administrateur du site."
},
"tag": {
"tag-created": "Mot-clé créé",
"tag-creation-failed": "La création du mot-clé a échoué",
"tag-deleted": "Mot-clé supprimé",
"tag-deletion-failed": "La suppression du mot-clé a échoué",
"tag-update-failed": "La mise à jour du mot-clé a échoué",
"tag-updated": "Mot-clé mis à jour",
"tags": "Mots-clés",
"untagged-count": "{count} sans mot-clés"
},
"user": {
"admin": "Administrateur",
"are-you-sure-you-want-to-delete-the-link": "Voulez-vous réellement supprimer le lien <b>{link}<b/> ?",
"are-you-sure-you-want-to-delete-the-user": "Voulez-vous réellement supprimer l'utilisateur <b>{activeName} ID : {activeId}<b/> ?",
"confirm-link-deletion": "Confirmer la suppression du lien",
"confirm-password": "Confirmer mot de passe",
"confirm-user-deletion": "Confirmer la suppression",
"could-not-validate-credentials": "La vérification de vos identifiants a échoué",
"create-link": "Créer un lien",
"create-user": "Créer utilisateur",
"current-password": "Mot de passe actuel",
"e-mail-must-be-valid": "Le courriel doit être valide",
"edit-user": "Modifier l'utilisateur",
"email": "Courriel",
"error-cannot-delete-super-user": "Erreur ! Impossible de supprimer le super utilisateur",
"existing-password-does-not-match": "Le mot de passe actuel ne correspond pas",
"full-name": "Nom",
"link-id": "ID du lien",
"link-name": "Nom du lien",
"login": "Connexion",
"logout": "Déconnexion",
"manage-users": "Gérer les utilisateurs",
"new-password": "Nouveau mot de passe",
"new-user": "Nouvel utilisateur",
"password": "Mot de passe",
"password-has-been-reset-to-the-default-password": "Le mot de passe a été réinitialisé à la valeur par défaut",
"password-must-match": "Les mots de passe doivent correspondre",
"password-reset-failed": "Échec de la réinitialisation du mot de passe",
"password-updated": "Mot de passe mis à jour",
"reset-password": "Réinitialiser le mot de passe",
"sign-in": "Se connecter",
"total-mealplans": "Nombre de menus",
"total-users": "Nombre d'utilisateurs",
"upload-photo": "Importer une photo",
"use-8-characters-or-more-for-your-password": "Utilisez au moins 8 caractères pour votre mot de passe",
"user": "Utilisateur",
"user-created": "Utilisateur créé",
"user-creation-failed": "La création de l'utilisateur a échoué",
"user-deleted": "Utilisateur supprimé",
"user-id": "ID utilisateur",
"user-id-with-value": "ID utilisateur : {id}",
"user-password": "Mot de passe de l'utilisateur",
"user-successfully-logged-in": "Connexion réussie",
"user-update-failed": "La mise à jour de l'utilisateur a échoué",
"user-updated": "Utilisateur mis à jour",
"username": "Nom d'utilisateur",
"users": "Utilisateurs",
"users-header": "UTILISATEURS",
"webhook-time": "Heure du Webhook",
"webhooks-enabled": "Webhooks activés",
"you-are-not-allowed-to-create-a-user": "Vous n'avez pas le droit de créer un utilisateur",
"you-are-not-allowed-to-delete-this-user": "Vous n'avez pas le droit de supprimer cet utilisateur"
}
}

View File

@@ -47,11 +47,11 @@
"database": "Base de données",
"delete-event": "Supprimer lévènement",
"new-notification-form-description": "Mealie utilise la bibliothèque Apprise pour générer des notifications. Elle propose de nombreux services à utiliser pour les notifications. Consultez leur wiki pour un guide complet sur la façon de créer l'URL de votre service. Si disponible, sélectionner le type de votre notification peut inclure des fonctionnalités supplémentaires.",
"new-version": "Nouvelle version disponible!",
"new-version": "Nouvelle version disponible !",
"notification": "Notification",
"refresh": "Rafraîchir",
"scheduled": "Planifié",
"something-went-wrong": "Une erreur s'est produite!",
"something-went-wrong": "Une erreur s'est produite !",
"subscribed-events": "Évènements suivis",
"test-message-sent": "Message de test envoyé"
},
@@ -60,7 +60,7 @@
"clear": "Effacer",
"close": "Fermer",
"confirm": "Confirmer",
"confirm-delete-generic": "Voulez-vous vraiment supprimer ceci?",
"confirm-delete-generic": "Voulez-vous vraiment supprimer ceci ?",
"copied": "Copié",
"create": "Créer",
"created": "Créé",
@@ -316,7 +316,7 @@
"current": "Version :",
"custom-pages": "Pages personnalisées",
"edit-page": "Modifier la page",
"events": "Evènements",
"events": "Évènements",
"first-day-of-week": "Premier jour de la semaine",
"group-settings-updated": "Paramètres du groupe mis à jour",
"homepage": {

View File

@@ -3,6 +3,7 @@ import Vuetify from "vuetify/lib";
Vue.use(Vuetify);
import ca from "vuetify/es5/locale/ca";
import de from "vuetify/es5/locale/de";
import en from "vuetify/es5/locale/en";
import es from "vuetify/es5/locale/es";
@@ -10,7 +11,10 @@ import fr from "vuetify/es5/locale/fr";
import hu from "vuetify/es5/locale/hu";
import it from "vuetify/es5/locale/it";
import nl from "vuetify/es5/locale/nl";
import no from "vuetify/es5/locale/no";
import pl from "vuetify/es5/locale/pl";
import ru from "vuetify/es5/locale/ru";
import sk from "vuetify/es5/locale/sk";
import sv from "vuetify/es5/locale/sv";
import zhHans from "vuetify/es5/locale/zh-Hans";
import zhHant from "vuetify/es5/locale/zh-Hant";
@@ -43,6 +47,8 @@ const vuetify = new Vuetify({
},
lang: {
locales: {
"ca-ES": ca,
"da-DK": en, // language not supported by Vuetify
"de-DE": de,
"en-US": en,
"en-GB": en,
@@ -51,7 +57,10 @@ const vuetify = new Vuetify({
"hu-HU": hu,
"it-IT": it,
"nl-NL": nl,
"no-NO": no,
"pl-PL": pl,
"ru-RU": ru,
"sk-SK": sk,
"sv-SE": sv,
"zh-CN": zhHans,
"zh-TW": zhHant,

View File

@@ -9,6 +9,14 @@ const state = {
name: "British English",
value: "en-GB",
},
{
name: "Català (Catalan)",
value: "ca-ES",
},
{
name: "Dansk (Danish)",
value: "da-DK",
},
{
name: "Deutsch (German)",
value: "de-DE",
@@ -29,6 +37,10 @@ const state = {
name: "Italiano (Italian)",
value: "it-IT",
},
{
name: "Norsk (Norwegian)",
value: "no-NO",
},
{
name: "Nederlands (Dutch)",
value: "nl-NL",
@@ -37,6 +49,14 @@ const state = {
name: "Polski (Polish)",
value: "pl-PL",
},
{
name: "Pусский (Russian)",
value: "ru-RU",
},
{
name: "Slovenčina (Slovak)",
value: "sk-SK",
},
{
name: "Svenska (Swedish)",
value: "sv-SE",

View File

@@ -151,6 +151,11 @@ class AppSettings(BaseSettings):
DEFAULT_EMAIL: str = "changeme@email.com"
DEFAULT_PASSWORD: str = "MyPassword"
LDAP_AUTH_ENABLED: bool = False
LDAP_SERVER_URL: str = None
LDAP_BIND_TEMPLATE: str = None
LDAP_ADMIN_FILTER: str = None
SCHEDULER_DATABASE = f"sqlite:///{app_dirs.DATA_DIR.joinpath('scheduler.db')}"
TOKEN_TIME: int = 2 # Time in Hours
@@ -167,6 +172,8 @@ class AppSettings(BaseSettings):
RECIPE_DISABLE_COMMENTS: bool = False
RECIPE_DISABLE_AMOUNT: bool = False
AUTO_BACKUP_ENABLED: bool = False
class Config:
env_file = BASE_DIR.joinpath(".env")
env_file_encoding = "utf-8"

View File

@@ -11,6 +11,44 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ALGORITHM = "HS256"
def user_from_ldap(session, username: str, password: str) -> UserInDB:
"""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
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(session, username, "username", any_case=True)
if not user:
user = db.users.create(
session,
{
"username": username,
"password": "LDAP",
# Fill the next two values with something unique and vaguely
# relevant
"full_name": username,
"email": username,
},
)
if settings.LDAP_ADMIN_FILTER:
user.admin = len(conn.search_s(user_dn, ldap.SCOPE_BASE, settings.LDAP_ADMIN_FILTER, [])) > 0
db.users.update(session, user.id, user)
return user
def create_access_token(data: dict(), expires_delta: timedelta = None) -> str:
to_encode = data.copy()
expires_delta = expires_delta or timedelta(hours=settings.TOKEN_TIME)
@@ -31,6 +69,8 @@ def authenticate_user(session, email: str, password: str) -> UserInDB:
if not user:
user = db.users.get(session, email, "username", any_case=True)
if settings.LDAP_AUTH_ENABLED and (not user or user.password == "LDAP"):
return user_from_ldap(session, email, password)
if not user:
return False

View File

@@ -6,7 +6,7 @@ from typing import Union
from jinja2 import Template
from mealie.core import root_logger
from mealie.core.config import app_dirs
from mealie.core.config import app_dirs, settings
from mealie.db.database import db
from mealie.db.db_setup import create_session
from mealie.services.events import create_backup_event
@@ -153,6 +153,9 @@ def backup_all(
def auto_backup_job():
if not settings.AUTO_BACKUP_ENABLED:
return
for backup in app_dirs.BACKUP_DIR.glob("Auto*.zip"):
backup.unlink()

View File

@@ -43,6 +43,7 @@ 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 +55,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 +73,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

1011
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ python-dotenv = "^0.15.0"
python-slugify = "^4.0.1"
requests = "^2.25.1"
PyYAML = "^5.3.1"
extruct = "^0.12.0"
extruct = "^0.13.0"
python-multipart = "^0.0.5"
fastapi-camelcase = "^1.0.2"
bcrypt = "^3.2.0"
@@ -32,8 +32,9 @@ lxml = "4.6.2"
Pillow = "^8.2.0"
pathvalidate = "^2.4.1"
apprise = "0.9.3"
recipe-scrapers = "^13.2.7"
recipe-scrapers = "^13.7.0"
psycopg2-binary = {version = "^2.9.1", optional = true}
python-ldap = "^3.3.0"
gunicorn = "^20.1.0"
[tool.poetry.dev-dependencies]
@@ -67,3 +68,4 @@ skip_empty = true
[tool.poetry.extras]
pgsql = ["psycopg2-binary"]
ldap = ["python-ldap"]

View File

@@ -33,4 +33,10 @@ RECIPE_SHOW_NUTRITION=False
RECIPE_SHOW_ASSETS=False
RECIPE_LANDSCAPE_VIEW=False
RECIPE_DISABLE_COMMENTS=False
RECIPE_DISABLE_AMOUNT=False
RECIPE_DISABLE_AMOUNT=False
# Configuration for authentication via an external LDAP server
LDAP_AUTH_ENABLED=False
LDAP_SERVER_URL=None
LDAP_BIND_TEMPLATE=None
LDAP_ADMIN_FILTER=None

View File

@@ -2,6 +2,8 @@ from pathlib import Path
from mealie.core import security
from mealie.routes.deps import validate_file_token
from mealie.core.config import settings
from mealie.db.db_setup import create_session
def test_create_file_token():
@@ -9,3 +11,37 @@ 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):
import ldap
user = "testinguser"
password = "testingpass"
bind_template = "cn={},dc=example,dc=com"
admin_filter = "(memberOf=cn=admins,dc=example,dc=com)"
monkeypatch.setattr(settings, "LDAP_AUTH_ENABLED", True)
monkeypatch.setattr(settings, "LDAP_SERVER_URL", "") # Not needed due to mocking
monkeypatch.setattr(settings, "LDAP_BIND_TEMPLATE", bind_template)
monkeypatch.setattr(settings, "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)
result = security.authenticate_user(create_session(), user, password)
assert result is not False
assert result.username == user