mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-26 17:53:12 -05:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8154ee548a | ||
|
|
591bf9d98f | ||
|
|
f202b1f922 | ||
|
|
17b2e37e4e | ||
|
|
0ec8087ac6 | ||
|
|
e580d6f904 | ||
|
|
56d9cafb68 | ||
|
|
32c864c703 | ||
|
|
37280a3da0 | ||
|
|
7f850fba98 | ||
|
|
b40f201430 | ||
|
|
a0d796551c |
38
.github/ISSUE_TEMPLATE/v1-task.yaml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/v1-task.yaml
vendored
Normal 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
|
||||
1
.github/workflows/test-all.yml
vendored
1
.github/workflows/test-all.yml
vendored
@@ -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'
|
||||
|
||||
@@ -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 \
|
||||
|
||||
28
docs/docs/changelog/v0.5.4.md
Normal file
28
docs/docs/changelog/v0.5.4.md
Normal 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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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>
|
||||
|
||||
21
frontend/src/locales/dateTimeFormats/fr-CA.json
Normal file
21
frontend/src/locales/dateTimeFormats/fr-CA.json
Normal 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"
|
||||
}
|
||||
}
|
||||
489
frontend/src/locales/messages/fr-CA.json
Normal file
489
frontend/src/locales/messages/fr-CA.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
1011
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user