feature/multi-tenancy and move caddy server (#980)

* update to GUIDs

* fix cookbook id relationships

* update webhook keys

* cleanup naming and attribute orders

* remove old database tables

* fix meal-plan images

* remove dashbaord and events api

* use recipe-id instead of id

* cleanup documentation assets

* cleanup docs for v1 beta-release

* add depends_on for docker-compose

* use docker volumes for examples

* move caddy to frontend container
This commit is contained in:
Hayden 2022-02-20 14:17:51 -09:00 committed by GitHub
parent 14cc541f7a
commit 602f248541
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 187 additions and 1170 deletions

View File

@ -1,35 +0,0 @@
{
auto_https off
admin off
}
:80 {
@proxied path /api/* /docs /openapi.json
@static {
file
path *.ico *.css *.js *.gif *.jpg *.jpeg *.png *.svg *.woff *.woff2 *.webp
}
encode gzip zstd
# Handles Recipe Images / Assets
handle_path /api/media/recipes/* {
header @static Cache-Control max-age=31536000
root * /app/data/recipes/
file_server
}
# Handles User Images
handle_path /api/media/users/* {
header @static Cache-Control max-age=31536000
root * /app/data/users/
file_server
}
handle @proxied {
uri strip_suffix /
reverse_proxy http://127.0.0.1:9000
}
}

View File

@ -37,14 +37,6 @@ RUN apt-get update \
# LDAP Dependencies
libsasl2-dev libldap2-dev libssl-dev \
gnupg gnupg2 gnupg1 \
debian-keyring \
debian-archive-keyring \
apt-transport-https \
&& curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | apt-key add - \
&& curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list \
&& apt-get update \
&& apt-get install --no-install-recommends -y \
caddy \
&& pip install -U --no-cache-dir pip
# install poetry - respects $POETRY_VERSION & $POETRY_HOME
@ -117,11 +109,6 @@ COPY --from=crfpp /usr/local/lib/ /usr/local/lib
COPY --from=crfpp /usr/local/bin/crf_learn /usr/local/bin/crf_learn
COPY --from=crfpp /usr/local/bin/crf_test /usr/local/bin/crf_test
# copying caddy into image
COPY --from=builder-base /usr/bin/caddy /usr/bin/caddy
# copy backend
COPY ./mealie $MEALIE_HOME/mealie
COPY ./poetry.lock ./pyproject.toml $MEALIE_HOME/
@ -135,15 +122,11 @@ WORKDIR $MEALIE_HOME
RUN . $VENV_PATH/bin/activate && poetry install -E pgsql --no-dev
WORKDIR /
# copy frontend
# COPY --from=frontend-build /app/dist $MEALIE_HOME/dist
COPY ./Caddyfile $MEALIE_HOME
# Grab CRF++ Model Release
RUN curl -L0 $CRF_MODEL_URL --output $MEALIE_HOME/mealie/services/parser_services/crfpp/model.crfmodel
VOLUME [ "$MEALIE_HOME/data/" ]
ENV APP_PORT=80
ENV APP_PORT=9000
EXPOSE ${APP_PORT}

View File

@ -7,13 +7,13 @@ services:
context: ./frontend
dockerfile: Dockerfile
restart: always
volumes:
- mealie-data:/app/data/
ports:
- 9091:3000
environment:
# - GLOBAL_MIDDLEWARE=auth
# - SUB_PATH=/mealie/
- ALLOW_SIGNUP=true
- API_URL=http://mealie-api:80
- API_URL=http://mealie-api:9000
# =====================================
# Light Mode Config
@ -25,7 +25,7 @@ services:
- THEME_LIGHT_WARNING=#FF6D00
- THEME_LIGHT_ERROR=#EF5350
# =====================================
# Light Mode Config
# Dark Mode Config
- THEME_DARK_PRIMARY=#E58325
- THEME_DARK_ACCENT=#007A99
- THEME_DARK_SECONDARY=#973542
@ -40,8 +40,10 @@ services:
target: production
dockerfile: Dockerfile
restart: always
volumes:
- mealie-data:/app/data/
ports:
- 9092:80
- 9092:9000
environment:
DB_ENGINE: sqlite # Optional: 'sqlite', 'postgres'
# =====================================
@ -74,3 +76,7 @@ services:
# environment:
# POSTGRES_PASSWORD: mealie
# POSTGRES_USER: mealie
volumes:
mealie-data:
driver: local

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 619 KiB

0
docs/docs/assets/img/home_screenshot.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 533 KiB

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@ -1,131 +0,0 @@
# Backup and Imports
All recipe data can be imported and exported as necessary from the UI. Under the admin page you'll find the section for using Backups and Exports.
!!! danger
As this is still a **BETA** it is recommended that you backup your data often and store in more than one place. Adhere to backup best practices with the [3-2-1 Backup Rule](https://en.wikipedia.org/wiki/Backup). Prior to upgrading you **should** perform a backup to limit any data loss.
!!! tip "Mealie data that is saved on backups"
- [x] Recipe Data
- [ ] Meal Plan
- [x] Site Settings
- [x] Custom Pages
- [x] User Data
- [x] Group Data
To create an export simply add the tag and the markdown template and click "Create" and your backup will be created on the server. The backup is a standard zipfile containing all the images, json files, and rendered jinaj2 templates for each recipe. To view the available variables, open a recipe in the json editor.
To import a backup it must be in your backups folder. If it is in the backup folder it will automatically show up as a source to restore from. Selected the desired backup and import the backup file. Backups can be uploaded from the UI as well as added on the file system.
## Demo
![](../../assets/gifs/backup-demo-v1.gif)
## API Examples
You can use the API to create and retrieve backups remotely from any server that can access the Mealie instance. This is useful for easily managing off-site backups via cron-job or other scheduled tasks. You can find interactive documentation for your API at https://your-mealie-instance.com/docs
### curl Example
Create a backup with curl
```bash
curl -X 'POST' \
'http://localhost:9000/api/backups/export/database' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"tag": "July 23rd 2021",
"options": {
"recipes": true,
"settings": true,
"themes": true
},
"templates": [
"recipes.md"
]
}'
```
### wget Example
First request a file token with curl:
```bash
curl -X 'GET' \
'http://localhost:9000/api/backups/{file_name}/download' \
-H 'accept: application/json' \
-H 'Content-Type: application/json'
```
Then download the file with wget:
```bash
wget http://localhost:9000/api/utils/download?token={fileToken}
```
## Jinja2 Templating
On export you can select a template to use to render files using the jinja2 syntax. This can be done to export recipes in other formats besides regular .json. Look at this example for rendering a markdown recipe using the jinja2 syntax.
### Input
```jinja2
![Recipe Image](../images/{{ recipe.image }})
# {{ recipe.name }}
{{ recipe.description }}
## Ingredients
{% for ingredient in recipe.recipeIngredient %}
- [ ] {{ ingredient }}
{% endfor %}
## Instructions
{% for step in recipe.recipeInstructions %}
- [ ] {{ step.text }}
{% endfor %}
{% for note in recipe.notes %}
**{{ note.title }}:** {{ note.text }}
{% endfor %}
---
Tags: {{ recipe.tags }}
Categories: {{ recipe.categories }}
Original URL: {{ recipe.orgURL }}
```
### Output
```markdown
![Recipe Image](../images/five-spice-popcorn-chicken.jpg)
# Five Spice Popcorn Chicken
Its easy to rely on take-out for some of our favorite Chinese dishes. However, with the right pantry staples, dishes like this Five Spice Popcorn Chicken can become part of your go-to arsenal of recipes. This crispy chicken is coated in a creamy, tangy sauce, made zesty with The Spice Hunter Chinese Five Spice, a blend of star anise, cloves, cinnamon, fennel, and black pepper.
## Ingredients
- [ ] 1 tablespoon soy sauce
- [ ] 1 tablespoon sugar
- [ ] 1 teaspoon The Spice Hunter® Chinese Five Spice Blend, plus more for serving
- [ ] 1 clove garlic, finely grated
- [ ] 1 1/2 pounds boneless skinless chicken thighs, cut into roughly 1-inch chunks
- [ ] ⅓ cup cornstarch
- [ ] 1 large egg, beaten
- [ ] ¾ cup all-purpose flour
- [ ] Canola or vegetable oil, for frying
- [ ] Flaky sea salt
- [ ] Scallion, thinly sliced, for serving
- [ ] Sriracha mayonnaise, for serving, optional
## Instructions
- [ ] In a medium bowl, whisk the soy sauce with the sugar, Chinese Five Spice, and garlic. Add the chicken and toss to coat. Let marinate 15 minutes.
- [ ] Drain any excess marinade off of the chicken and toss the chicken with the cornstarch to coat. Once fully coated, add the beaten egg and toss to coat.
- [ ] In a large heavy bottomed pot, heat 1-inch of oil to 350.
- [ ] Place the flour in a large ziploc bag. Working in batches, transfer a few chicken pieces into the bag with the flour and toss to coat, then remove, leaving excess flour in the bag.
- [ ] Carefully place the breaded chicken in the hot oil and fry, turning occasionally, until golden and cooked through about 3 to 4 minutes.
- [ ] Using a slotted spoon or spider, transfer the cooked chicken to a paper towel lined plate. Season with salt and additional Chinese Five Spice seasoning. Repeat the flouring and frying with remaining chicken.
- [ ] Serve with scallions, more Chinese Five Spice Blend, and optional sriracha mayonnaise.
---
Tags: []
Categories: []
Original URL: https://www.bonappetit.com/recipe/five-spice-popcorn-chicken#intcid=_bon-appetit-recipe-bottom-recirc_3cad5ce9-734a-46f8-b503-78c33d2e7279_similar2-3
```
If you decide you don't like mealie: This is a good way to export into a format that can be imported into another.

View File

@ -1,13 +0,0 @@
# Building Pages
!!! warning
The page building is still experimental and may change. You can provide feedback on any changes you'd like to see on [github](https://github.com/hay-kot/mealie/discussions/229)
Custom pages can be created to organize multiple categories into a single page. Links to your custom pages are displayed on the home page sidebar and accessible by all users, however only Administrators can create or update pages.
![custom page](../../assets/img/custom-page.webp)
To create a new page navigate to the settings page at `/admin/settings` and scroll down to the custom pages section. Here you can create, view, and edit your custom pages. To reorder how they are displayed on the sidebar you can drag and drop the pages into the preferred order.
![create custom page](../../assets/gifs/create-custom-page-demo.gif)
!!! tip
To save the order of pages you must click the save button displayed on the bottom of the Custom Page section. This is not necessary for updating individual page data.

View File

@ -1,16 +0,0 @@
#Dashboard
The dashboard gives you a quick overview of how your Mealie is doing. There is a 'Recipes' card, an overview of the users and groups, an 'Events' card and a 'Backups' card.
![dashboard](../../assets/img/dashboard.webp)
## Recipes
'Recipes' shows how many recipes you have in your catalogue but also checks if you have any untagged or uncategorized ones. If you click on one of these, you are redirected to the Toolbox where you can further organize these recipes.
## Users
This gives an overview of the total number of users for your Mealie instance. The 'manage users' and 'manage groups' button will redirect you to the [user-management page](../admin/user-management.md).
## Backups
Here you can choose to import an older backup from a previous instance. You could also create a custom backup with your own tag where you can choose for a full backup or only some parts like recipes, users,...
If you click 'create', an automatic backup is generated.

View File

@ -1,24 +0,0 @@
# Migration
## Chowdown
To migrate recipes from a Chowdown
1. Download the code repository as a .zip file
2. Upload the .zip file in the Chowdown section in Mealie
3. Select import on the newly available migration.
## Nextcloud Recipes
Nextcloud recipes can be imported from a zip file that contains the data stored in Nextcloud. The zip file can be uploaded from the frontend or placed in the data/migrations/nextcloud directory. See the example folder structure below to ensure your recipes are able to be imported.
```
nextcloud_recipes.zip
├── recipe_1
│ ├── recipe.json
│ ├── full.jpg
│ └── thumb.jpg
├── recipe_2
│ ├── recipe.json
│ └── full.jpg
└── recipe_3
└── recipe.json
```

View File

@ -1,15 +0,0 @@
# Site Settings
Your sites settings panel can only be accessed by administrators. This is where you can customize your site for all users.
## Home Page Settings
| Option | Description |
| --------------------- | ------------------------------------------------------------------------------------------------------------- |
| Show Recent | To display the recent recipes section on the home page |
| Card Per Section | The amount of cards displayed in each section on the home page |
| Home Page Sections | Category sections to include on the home page |
| Language | The default site language |
| First day of the week | The default start day of the week used in Meal Plans |
| Custom Pages | Create a [custom page](../admin/building-pages.md) which appears in the sidebar with the categories you chose |
![Site Settings Image](../../assets/img/site-settings.webp)

View File

@ -1,86 +0,0 @@
# User Management
As of version v0.4.0 users have limited functionality, but they will offer more permissions and structure as time goes on. To understand the different systems, see each section below. Note, that by default all users will be assigned the default group. If you're only managing one household you won't need to do anything to set up a new group.
!!! summary "Users and Groups"
=== ":fontawesome-solid-user-cog: Admins"
Mealie admins are super users that have access to all user data (excluding passwords). All admins can perform administrative tasks like adding users, resetting user passwords, backing up the database, migrating data, and managing site settings.
**Admins Can**
- All User Actions
- Adjust Site Settings
- Create and Update Users
- Create and Update Groups
- Generate User Sign-up Links
- Migrate Data from other Services
- Backup Site Data
=== ":fontawesome-solid-user: Users"
A single user created by an Admin that has basic privileges to edit their profile, create and edit recipes.
**Users Can**
- Manage Their Profile
- Create, Edit, and Update Recipes
- Create, Edit, and Update Mealplans *(By Group)*
- Set Mealplan Categories
- Create and Schedule Webhooks *(By Group)*
=== ":fontawesome-solid-users: Groups"
User groups are a collection of users that are associated together. Typically used for separate households sharing a single instance.
**Groups Share**
- Mealplans
- Mealplan Settings
- Webhooks
!!! warning
As of v0.4.0 any authenticated user is able to perform any action on the backend server through the API. To limit a standard users scope, the pages on the frontend are limited. Proper support for permission structures on the backend API will come in a later version.
## Startup
On the first startup you'll need to login to Mealie using the default username and password `changeme@email.com` and `MyPassword` or the default set through the env variable. On first login you'll be required to reset your password. After resetting your password you should also change your email address as appropriate. This will be used for logins on all future requests.
!!! tip
Your default password environment variable will be the default password for all new users that are created. This is stored in plain text and should not be used **anywhere** else.
## Creating and Editing Users
There are two ways to create users in Mealie.
### Manually Creating a User
In the Manage Users section you are able to create a user by providing the necessary information in the pop-up dialog.
![Create User Image](../../assets/img/add-user.webp){: align=right style="height:50%;width:50%"}
- User Name
- Email
- User Group
- If they are an Admin
When creating users manually, their password will be set from the default assigned by the ENV variable.
### Sign Up Links
You can generate sign-up links in the Manage Users section. Select the "create link" button and provide the name of the link and if the user will be an administrator. Once a link is created it will populate in the table where you'll be able to see all active links, delete a link, and copy the link as needed.
![Sign Up Links Image](../../assets/img/sign-up-links.webp)
!!! tip
When a link is used it is automatically removed from the database.
## Creating Groups
You can easily create and manage groups via the frontend in the admin panel under "Manage Users". Navigate to the groups tab and you'll find a "create group" button as well as a list of all groups in your database. To create a group, select the "create group" button and provide a name for the new group. Once created you can now assign users to the new group.
![Group Management Panel](../../assets/img/group-manager.png)
!!! tip
User Groups can only be deleted if no users are a part of the group. If you want to delete a group, you must assign the users to another group before removing.
## Password Reset
If a user forgets their password an administrator is able to reset their password through the user management page. In the user table, select edit. In the popup window click the "Reset Password" to reset a user's password to the default. This is either 'MyPassword' or set through an environment variable. See the [Installation Page](../getting-started/install.md) for more details on environment variables.

View File

@ -1,13 +1,15 @@
# Using iOS Shortcuts with Mealie
!!! info
This guide was submitted by a community member. Find something wrong? Submit a PR to get it fixed!
![Image from apple site](https://help.apple.com/assets/5E8CEA35094622DF10489984/5E8CEA42094622DF1048998D/en_US/ed1f9c157cdefc13e0161e0f70015455.png)
User [brasilikum](https://github.com/brasilikum) opened an issue on the main repo about how they had created an [iOS shortcut](https://github.com/hay-kot/mealie/issues/103) for interested users. This is a useful utility for iOS users who browse for recipes in their web browser from their devices. Recent updates to Mealie has made this original shortcut break. Reddit user [BooNooBooNooB](https://www.reddit.com/user/BooNooBooNooB/) has helped to create a new working version.
User [brasilikum](https://github.com/brasilikum) opened an issue on the main repo about how they had created an [iOS shortcut](https://github.com/hay-kot/mealie/issues/103) for interested users. This is a useful utility for iOS users who browse for recipes in their web browser from their devices. Recent updates to Mealie has made this original shortcut break. Reddit user [BooNooBooNooB](https://www.reddit.com/user/BooNooBooNooB/) has helped to create a new working version.
Don't know what an iOS shortcut is? Neither did I! Experienced iOS users may already be familiar with this utility but for the uninitiated, here is the official Apple explanation:
> A shortcut is a quick way to get one or more tasks done with your apps. The Shortcuts app lets you create your own shortcuts with multiple steps. For example, build a “Surf Time” shortcut that grabs the surf report, gives an ETA to the beach, and launches your surf music playlist.
>
Basically it is a visual scripting language that lets a user build an automation in a guided fashion. The automation can be [shared with anyone](https://www.icloud.com/shortcuts/4c40fcc6f39549f9a189995a449cd44f) but if it is a user creation, you'll have to jump through a few hoops to make an untrusted automation work on your device.

View File

@ -15,4 +15,3 @@ Recipes extras are a key feature of the Mealie API. They allow you to create cus
For example you could add `{"message": "Remember to thaw the chicken"}` to a recipe and use the webhooks built into mealie to send that message payload to a destination to be processed.
![api-extras-gif](../../assets/gifs/api-extras.gif)

View File

@ -14,6 +14,8 @@ services:
mealie-frontend:
image: hkotel/mealie:frontend-nightly
container_name: mealie-frontend
depends_on:
- mealie-api
environment:
# Set Frontend ENV Variables Here
- ALLOW_SIGNUP=true
@ -21,11 +23,15 @@ services:
restart: always
ports:
- "9925:3000"
volumes:
- mealie-data:/app/data/
mealie-api:
image: hkotel/mealie:api-nightly
container_name: mealie-api
depends_on:
- postgres
volumes:
- ./data/:/app/data
- mealie-data:/app/data/
environment:
# Set Backend ENV Variables Here
- PUID=1000
@ -33,7 +39,7 @@ services:
- TZ=America/Anchorage
- MAX_WORKERS=1
- WEB_CONCURRENCY=1
- BASE_URL=https://beta.mealie.io
- BASE_URL=https://mealie.yourdomain.com
# Database Settings
- DB_ENGINE=postgres
@ -50,5 +56,9 @@ services:
environment:
POSTGRES_PASSWORD: mealie
POSTGRES_USER: mealie
volumes:
mealie-data:
driver: local
```

View File

@ -21,11 +21,13 @@ services:
restart: always
ports:
- "9925:3000"
volumes:
- mealie-data:/app/data/
mealie-api:
image: hkotel/mealie:api-nightly
container_name: mealie-api
volumes:
- ./data/:/app/data
- mealie-data:/app/data/
environment:
# Set Backend ENV Variables Here
- PUID=1000
@ -33,6 +35,10 @@ services:
- TZ=America/Anchorage
- MAX_WORKERS=1
- WEB_CONCURRENCY=1
- BASE_URL=https://beta.mealie.io
- BASE_URL=https://mealie.yourdomain.com
restart: always
volumes:
mealie-data:
driver: local
```

View File

@ -1,14 +1,18 @@
# About The Project
!!! warning "Mealie v1 Beta Release"
This documentation if for the Mealie v1 Beta release and is not final. As such, it may contain incomplete or incorrect information. You should understand that installing Mealie v1 Beta is a work in progress and while we've committed to maintaining the database schema and provided migrations, we are still in the process of adding new features, and robust testing to ensure the application works as expected.
You should likely find bugs, errors, and unfinished pages within the application. To find the current status of the release you can checkout the [project on github](https://github.com/hay-kot/mealie/projects/7) or reach out on discord.
You should also be aware that Mealie v1 Beta does not have the backup/export feature available. This is the next priority for Mealie v1
and is currently being worked out.
Mealie is 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. Easily add recipes into your database by providing the url and Mealie will automatically import the relevant data or add a family recipe with the UI editor. Mealie also provides an API for interactions from 3rd party applications.
[Remember to join the Discord](https://discord.gg/QuStdQGSGK)
!!! note
In some of the demo gifs the styling may be different than the finale application. demos were done during development prior to finale styling.
!!! warning
This is a **BETA** release and that means things may break and or change down the line. I'll do my best to make sure that any API changes are thoughtful and necessary in order not to break things. Additionally, I'll do my best to provide a migration path if the database schema ever changes. Do not use programs like watchtower to auto update your container. You **WILL** run into issues if you do this!
## Key Features

View File

@ -0,0 +1,39 @@
# Migrating to Mealie v1 Release
The version 1 release of Mealie should be seen as an entirely different application. A whole host of changes have been made to improve the application, performance, and developer experience. Most of these improvements required significant breaking changes in the application that made a clean and easy migration impossible. However, if you've used Mealie prior to v1 there is a migration path to get most of your data from the old version to the new v1 version.
!!! info "Currently Supported Migration Data"
Supporting more data is a work in progress, but not a current priority. I'm open to PR's to add support for additional data.
- [x] Recipes
- [x] Categories
- [x] Tags
- [ ] Users
- [ ] Groups
- [ ] Meal Plans
- [ ] Cookbooks / Pages
## Step 1: Setting Up The New Application
Given the nature of the upgrade, it is highly recommended that you standup a new instance of mealie along side your current instance. This will allow you to migrate your data safely and quickly without any issues. Follow the instructions in the [Installation Checklist](../getting-started/installation/installation-checklist.md) to get started. Once that's complete and you can login, continue here with step 2.
## Step 2: Exporting Your Data from Pre-v1
In your instance of Mealie prior to v1, perform an export of your data in the Admin section. Be sure to include the recipes when performing the export. Checking additional items won't impact the migration, but they will be ignored if they are included.
## Step 3: Using the Migration Tool
In your new v1 instance, navigate to `/user/group/data/migrations` and select "Mealie" from the dropdown selector. Upload the exported data from your pre-v1 instance. Optionally, you can tag all the recipes you've imported with the `mealie_alpha` tag to help you identify them. Once the upload has finished, submit the form and your migration will begin. This may take some time, but when it's complete you'll be provided a new entry in hte "Previous Migrations" table below. Be sure to review the migration report to make sure everything was successful. There may be instances where some of the recipes were not imported, but the migration will still import all the successful recipes.
In most cases, it's faster to manually migrate the recipes that didn't take instead of trying to identify why the recipes failed to import. If you're experiencing issues with the migration tool, please open an issue on GitHub.
!!! note "Recipe Owners"
When perform any migration, it will automatically assign the owner of the recipe to the user that performed the migration. All group members will still be able to access the recipe, however the owner has special permissions to lock the recipe from edits from other users.
## Step 4: Reviewing New Features
v1 Comes with a whole host of new features and improvements. Checkout the changelog to get a sense for what's new.
- [v1 Changelog](../../changelog/v1.0.0.md)

View File

@ -1,23 +0,0 @@
# Organizing Recipes
Below are some general guidelines that were considered when creating the organization structure for recipes.
## From The Community
> My categories are mostly based on the 'course' they belong to. Appetizers, Starters, Main course, but also sauces or beverages. When I'm looking for an idea for an every day dinner, I just browse "main course".
>
> My tags are for picking the exact type of meal I'm looking for, based on my mood or my guests' diet, like gluten-free, vegetarian, sweet-sour or casserole. They can also act as sub-categories, like "alcohol" for beverages or "hot meal" for a main course.
>
> User: [sephrat](https://github.com/sephrat)
## Structure
!!! tip
Below is a suggestion of guidelines my wife and I use for organizing our recipes within Mealie. Mealie is fairly flexible, so feel free to utilize how you'd like! 👍
In the diagram below you will see what we came up with using the new custom pages feature. The large circles indicate pages, and the rectangles indicate categories. We've grouped several 'like' categories with each other as a way to quickly find similar items.
![Mealie Diagram](../../assets/img/mealie-diagram.webp)

View File

@ -1,139 +0,0 @@
# Recipes
## URL Import
Adding a recipe can be as easy as clicking in the bottom-right corner, copying the recipe URL into Mealie and letting the web scraper organize information. Currently this scraper is implemented with [recipe-scrapers](https://github.com/hhursev/recipe-scrapers). You may have mixed results on some websites, especially with blogs or non-specific recipe websites. See the bulk import Option below for another a convenient way to add blog style recipes into Mealie.
!!! tip
You can find a list of some of the supported sites in the recipe-scrapers repo. If you're site isn't supported, you can work with the recipe-scrapers team to implement it and we can down-stream those changes into Mealie.
![](../../assets/gifs/URL-import.gif)
## Using Bookmarklets
You can use bookmarklets to generate a bookmark that will take your current location, and open a new tab that will try to import that URL into Mealie.
You can use a [bookmarklet generator site](https://caiorss.github.io/bookmarklet-maker/) and the code below to generate a bookmark for your site. Just change the `http://localhost:8080` to your sites web address and follow the instructions. Note that there is no trailing `/`.
```js
var url = document.URL ;
var mealie = "http://localhost:8080"
var dest = mealie + "/?recipe_import_url=" + url
window.open(dest, '_blank')
```
## Recipe Editor
![edit-recipe](../../assets/img/edit-recipe.webp){: align=right style="height:225px;width:275px"}
Recipes can be edited and created via the UI. This is done with both a form based approach where you have a UI to work with as well as with a in browser JSON Editor. The JSON editor allows you to easily copy and paste data from other sources.
You can also add a custom recipe with the UI editor built into the web view. Using the `+` button on the site.
### Recipe Settings
Settings for a specific recipe can be adjusted in the settings menu inside the editor. Currently the settings supports
- Settings a Recipe to Public/Private
- Show Nutrition Values
- Show Assets
- Landscape Mode (Coming Soon)
!!! note
Recipes set to private will only be displayed when a user is logged in. Currently there is no way to generate a share-link for a private recipe, but it is on the roadmap.
### Recipe Assets
While in the editor you also have an opportunity to upload any asset to your recipe. There are several icons that you can choose from or you can choose an arbitrary file icon. Once uploaded you can view or download the asset when viewing the page.
!!! tip
You can get a link to an asset to embed in a recipe step by select the copy icon in editor mode.
### Bulk Import
Mealie also supports bulk import of recipe instructions and ingredients. Select "Bulk Add" in the editor and paste in your plain text data to be parsed. Each line is treated as one entry and will be appended to the existing ingredients or instructions if they exist. Empty lines will be stripped from the text.
![](../../assets/gifs/bulk-add-demo.gif)
## Schema
Recipes are stored in the json-like format in mongoDB and then sent and edited in json format on the frontend. Each recipes uses [Recipe Schema](https://schema.org/Recipe) as a general guide with some additional properties specific to Mealie.
### Example
```json
{
"id": 263,
"name": "Homemade Mac and Cheese Bites",
"slug": "homemade-mac-and-cheese-bites",
"image": "no image",
"description": "These are so simple and the perfect finger food ideal for serving kids and as an appetizer! These are DELICIOUS",
"recipeCategory": [],
"tags": [],
"rating": null,
"dateAdded": "2021-06-07",
"dateUpdated": "2021-06-07T16:23:10.254840",
"recipeYield": null,
"recipeIngredient": [
"½ pound elbow macaroni",
"2 Tablespoons butter",
"2 Tablespoons flour",
"1½ cups milk",
"2 cups shredded sharp cheddar cheese",
"2 ounces cream cheese",
"½ teaspoon salt",
"¼ teaspoon pepper",
"1 egg beaten"
],
"recipeInstructions": [
{
"title": "Preperation",
"text": "Preheat oven to 400 degrees F."
},
{
"title": "",
"text": "Spray mini muffin tins with cooking spray."
},
{
"title": "",
"text": "Cook pasta according to packaged directions and set aside."
},
{
"title": "Cooking",
"text": "In a medium size pan combine butter and flour over medium heat. Whisk until butter is melted and mixture is smooth. Slowly add milk and bring to a simmer. Add 1½ cups of the cheese, cream cheese, salt and pepper. Stir until smooth and creamy. Remove from heat."
},
{
"title": "",
"text": "In a large mixing bowl, carefully stir pasta, cheese sauce, and egg until evenly mixed and pasta is evenly coated."
},
{
"title": "",
"text": "Spoon mac and cheese into mini muffin tins and top each with a small pinch of remaining cheese. Bake at 400 degrees F for 15 minutes or until golden brown."
},
{
"title": "",
"text": "Allow to cool 5 minutes before gently removing the bites."
}
],
"nutrition": {
"calories": null,
"fatContent": null,
"proteinContent": null,
"carbohydrateContent": null,
"fiberContent": null,
"sodiumContent": null,
"sugarContent": null
},
"tools": [],
"totalTime": null,
"prepTime": null,
"performTime": null,
"settings": {
"public": true,
"showNutrition": true,
"showAssets": true,
"landscapeView": true
},
"assets": [],
"notes": [],
"orgURL": "https://www.chef-in-training.com/homemade-mac-and-cheese-bites/",
"extras": {},
"comments": []
}
```

View File

@ -1,59 +0,0 @@
# External Notifications
## Apprise
Using the [Apprise](https://github.com/caronc/apprise/) library Mealie is able to provide notification services for nearly every popular service. Some of our favorites are...
- [Gotify](https://github.com/caronc/apprise/wiki/Notify_gotify)
- [Discord](https://github.com/caronc/apprise/wiki/Notify_discord)
- [Home Assistant](https://github.com/caronc/apprise/wiki/Notify_homeassistant)
- [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix)
- [Pushover](https://github.com/caronc/apprise/wiki/Notify_pushover)
But there are many more to choose from! Take a look at their wiki for information on how to create their URL formats that you can use to create a notification integration in Mealie.
## Subscribe Events
There are several categories of events that mealie logs that can be broadcast with the notifications feature. You can also see a feed of your events in the Admin Dashboard
- General Events
- Application Startup
- Recipe Events
- Create Recipe
- Delete Recipe
- Database Events
- Export/Import
- Database Initialization
- Scheduled Events
- MealPlan Webhooks Sent
- Group Events
- Create/Delete Groups
- User Events
- User Creation
- User Sign-up
- Sign-up Token Creation
- Invalid login attempts
In most cases the events will also provide details on which user performed the action. Now you'll know when your grandma deletes your favorite recipe!
!!! info
This is a new feature and we are still working through all the possibilities of events. if you have an idea for an event let us know!
## Creating a New Notification
New events can be created and viewed in admin Toolbox `/admin/toolbox?tab=event-notifications`. Select the "+ Notification" button and you'll be provided with a dialog. Complete the form using the URL for the service you'd like to connect to. Before saving be sure to use the test feature.
!!! tip
The feedback provided from the test feature indicates only if the URL you provided is valid, not if the message was successfully sent. Be sure to check the notification feed for the test message.
![Add Notification Image](../../assets/img/add-notification.webp)
### Examples
#### Discord
![Discord](../../assets/img/discord-notification-example.webp)
#### Gotify
![Gotify](../../assets/img/gotify-notification-example.webp)

View File

@ -1,17 +0,0 @@
#Toolbox
The toolbox gives you multiple options to clean-up and organize your recipes. You can get notified through different channels.
You can access it through the 'Settings' menu or through the [dashboard](../admin/dashboard.md).
## Category and Tag Editor
The 'Categories' and 'Tags' tab give you the option to bulk assign categories and tags to multiple recipes. You could also remove the unused ones or title case them all.
![Toolbox-Categories](../../assets/img/Toolbox-Categories.webp)
## Bulk Organize
The 'Organize' tab can be used to show all of the items that do not have any category or tag assigned.
![Toolbox-Organize](../../assets/img/Toolbox-Organize.webp)

View File

@ -1,15 +0,0 @@
# Meal Planner
## Working with Planner
In Mealie you can create a meal plan based off the calendar inputs on the meal planner page. There is no limit to how long or how short a meal plan is. You may also create duplicate meal plans for the same date range. After selecting your date range, click on the card for each day and search through recipes to find your choice. Add a side-dish if you prefer to. After selecting a recipe for all meals, save the plan. Selecting the 'No Recipe' button will allow you to add an entry without a recipe by providing a title and description.
You can also randomly generate meal plans with the dice-button at the bottom.
To edit the meal in a meal plan simply select the edit button on the card in the timeline. Similarly, to delete a meal plan click the delete button on the card in the timeline. Currently, there is no support to change the date range in a meal plan.
## Shopping Lists
For any meal plan created you can view a breakdown of all the ingredients and use an experimental sort function to sort similarly ingredients. This is a very new feature and results of the auto sort may vary.
![](../../assets/gifs/meal-planner-demoV3.gif)

View File

@ -1,33 +0,0 @@
# User Settings
## Profile Settings
In the users profile they are able to:
- Change Display Name
- Change Email
- Update Password
- View Their Group
- Update Profile Picture (Experimental)
- Create API Keys
## Themes
Color themes can be created and set from the UI in the Settings-Profile page. You can select an existing color theme or create a new one. On creation of a new color theme, the default colors will be used, then you can select and save as you'd like. By default, the "default" theme will be loaded for all new users visiting the site. All created color themes are available to all users of the site. Theme Colors will be set for both light and dark modes.
![](../../assets/gifs/theme-demo-v3.gif)
!!! tip
Theme data is stored in local storage in the browser. Calling "Save colors and apply theme will refresh the local storage with the selected theme as well save the theme to the database.
## Group & Meal Plan
In the meal planner section a user can select categories to be used as a part of the random recipe selector in the meal plan creator. If no categories are selected, all recipes will be used
Meal planner webhooks are post requests sent from Mealie to an external endpoint. The body of the message is the Recipe JSON of the scheduled meal. If no meal is schedule, no request is sent. The webhook functionality can be enabled or disabled as well as scheduled. Note that you must "Save" prior to any changes taking affect server side.
## API Key Generation
Users can quickly and easily generate API keys with the user interface. Provide a name for your token and then you are shown 1 time the generated API key. If you ever loose the API key you are not able to identify or retrieve it from the UI.
![API Key Image](../../assets/img/api-key-image-v1.webp)
!!! warning
API keys are stored in plain text in the database.

File diff suppressed because one or more lines are too long

View File

@ -55,8 +55,10 @@ nav:
- Getting Started:
- Introduction: "documentation/getting-started/introduction.md"
- Updating: "documentation/getting-started/updating.md"
- Version 1 Migration: "documentation/getting-started/migrating-to-mealie-v1.md"
- FAQ: "documentation/getting-started/faq.md"
- API: "documentation/getting-started/api-usage.md"
- Road Map: "documentation/getting-started/roadmap.md"
- Installation:
- Installation Checklist: "documentation/getting-started/installation/installation-checklist.md"
@ -64,27 +66,6 @@ nav:
- PostgreSQL: "documentation/getting-started/installation/postgres.md"
- Frontend Configuration: "documentation/getting-started/installation/frontend-config.md"
- Backend Configuration: "documentation/getting-started/installation/backend-config.md"
- Advanced: "documentation/getting-started/installation/advanced.md"
- Recipes:
- Working With Recipes: "documentation/recipes/recipes.md"
- Organizing Recipes: "documentation/recipes/organizing-recipes.md"
- Users & Groups:
- User Settings: "documentation/users-groups/user-settings.md"
- Planning Meals: "documentation/users-groups/meal-planner.md"
- Admin:
- Dashboard: "documentation/admin/dashboard.md"
- Site Settings: "documentation/admin/site-settings.md"
- Building Pages: "documentation/admin/building-pages.md"
- User Management: "documentation/admin/user-management.md"
- Backups and Restore: "documentation/admin/backups-and-exports.md"
- Recipe Migration: "documentation/admin/migration-imports.md"
- Toolbox:
- External Notifications: "documentation/toolbox/notifications.md"
- Organization Tools: "documentation/toolbox/organize-tools.md"
- Community Guides:
- iOS Shortcuts: "documentation/community-guide/ios.md"
@ -105,8 +86,6 @@ nav:
- Guides:
- Improving Ingredient Parser: "contributors/guides/ingredient-parser.md"
- Development Road Map: "roadmap.md"
- Change Log:
- v1.0.0 A Whole New App: "changelog/v1.0.0.md"
- v0.5.2 Misc Updates: "changelog/v0.5.2.md"

View File

@ -3,8 +3,8 @@
admin off
}
:80 {
@proxied path /api/* /docs /openapi.json
:3000 {
@apidocs path /docs /openapi.json
@static {
file
@ -20,15 +20,21 @@
file_server
}
handle @proxied {
# Handles User Images
handle_path /api/media/users/* {
header @static Cache-Control max-age=31536000
root * /app/data/users/
file_server
}
handle @apidocs {
uri strip_suffix /
reverse_proxy http://mealie-api
reverse_proxy http://mealie-api:9000
}
handle {
header @static Cache-Control max-age=31536000
root * /app/dist
try_files {path}.html {path} /
file_server
uri strip_suffix /
reverse_proxy http://127.0.0.1:3001
}
}
}

View File

@ -23,12 +23,16 @@ RUN rm -rf node_modules && \
FROM node:16-alpine
RUN apk add caddy
WORKDIR /app
# copying caddy into image
COPY --from=builder /app .
COPY ./Caddyfile /app/
ENV HOST 0.0.0.0
EXPOSE 3000
CMD [ "yarn", "start" ]
RUN chmod +x /app/run.sh
ENTRYPOINT /app/run.sh

View File

@ -1,49 +0,0 @@
import { BaseAPI } from "../_base";
export type EventCategory = "general" | "recipe" | "backup" | "scheduled" | "migration" | "group" | "user";
export interface Event {
id?: number;
title: string;
text: string;
timeStamp?: string;
category?: EventCategory & string;
}
export interface EventsOut {
total: number;
events: Event[];
}
const prefix = "/api";
const routes = {
aboutEvents: `${prefix}/about/events`,
aboutEventsNotifications: `${prefix}/about/events/notifications`,
aboutEventsNotificationsTest: `${prefix}/about/events/notifications/test`,
aboutEventsId: (id: number) => `${prefix}/about/events/${id}`,
aboutEventsNotificationsId: (id: number) => `${prefix}/about/events/notifications/${id}`,
};
export class EventsAPI extends BaseAPI {
/** Get event from the Database
*/
async getEvents() {
return await this.requests.get<EventsOut>(routes.aboutEvents);
}
/** Get event from the Database
*/
async deleteEvents() {
return await this.requests.delete(routes.aboutEvents);
}
/** Delete event from the Database
*/
async deleteEvent(id: number) {
return await this.requests.delete(routes.aboutEventsId(id));
}
/** Get all event_notification from the Database
*/
}

View File

@ -1,7 +1,6 @@
import { RecipeAPI } from "./class-interfaces/recipes";
import { UserApi } from "./class-interfaces/users";
import { GroupAPI } from "./class-interfaces/groups";
import { EventsAPI } from "./class-interfaces/events";
import { BackupAPI } from "./class-interfaces/backups";
import { UploadFile } from "./class-interfaces/upload";
import { CategoriesAPI } from "./class-interfaces/organizer-categories";
@ -30,7 +29,6 @@ class Api {
public recipes: RecipeAPI;
public users: UserApi;
public groups: GroupAPI;
public events: EventsAPI;
public backups: BackupAPI;
public categories: CategoriesAPI;
public tags: TagsAPI;
@ -79,7 +77,6 @@ class Api {
this.multiPurposeLabels = new MultiPurposeLabelsApi(requests);
// Admin
this.events = new EventsAPI(requests);
this.backups = new BackupAPI(requests);
// Utils

View File

@ -24,7 +24,6 @@
</v-app>
</template>
<script lang="ts">
import { defineComponent, ref, useContext, onMounted } from "@nuxtjs/composition-api";
import AppHeader from "@/components/Layout/AppHeader.vue";
@ -45,11 +44,11 @@ export default defineComponent({
});
const topLinks: SidebarLinks = [
{
icon: $globals.icons.viewDashboard,
to: "/admin/dashboard",
title: i18n.t("sidebar.dashboard"),
},
// {
// icon: $globals.icons.viewDashboard,
// to: "/admin/dashboard",
// title: i18n.t("sidebar.dashboard"),
// },
{
icon: $globals.icons.cog,
to: "/admin/site-settings",
@ -115,6 +114,3 @@ export default defineComponent({
},
});
</script>

View File

@ -142,7 +142,7 @@ export default defineComponent({
{
icon: this.$globals.icons.cog,
title: this.$t("general.settings"),
to: "/admin/dashboard",
to: "/admin/site-settings",
restricted: true,
},
],

View File

@ -1,158 +0,0 @@
<template>
<v-container v-if="statistics" class="mt-10">
<v-row v-if="statistics">
<v-col cols="12" sm="12" md="4">
<BaseStatCard :icon="$globals.icons.primary">
<template #after-heading>
<div class="ml-auto text-right">
<h2 class="body-3 grey--text font-weight-light">
{{ $t("general.recipes") }}
</h2>
<h3 class="display-2 font-weight-light text--primary">
<small> {{ statistics.totalRecipes }}</small>
</h3>
</div>
</template>
<template #actions>
<div class="d-flex row py-2 justify-end">
<v-btn class="ma-1" small color="primary" to="/admin/toolbox/organize">
<v-icon left> {{ $globals.icons.tags }} </v-icon>
{{ $tc("tag.untagged-count", [statistics.untaggedRecipes]) }}
</v-btn>
<v-btn class="ma-1" small color="primary" to="/admin/toolbox/organize">
<v-icon left> {{ $globals.icons.tags }} </v-icon>
{{ $tc("category.uncategorized-count", [statistics.uncategorizedRecipes]) }}
</v-btn>
</div>
</template>
</BaseStatCard>
</v-col>
<v-col cols="12" sm="12" md="4">
<BaseStatCard :icon="$globals.icons.user">
<template #after-heading>
<div class="ml-auto text-right">
<h2 class="body-3 grey--text font-weight-light">
{{ $t("user.users") }}
</h2>
<h3 class="display-2 font-weight-light text--primary">
<small> {{ statistics.totalUsers }}</small>
</h3>
</div>
</template>
<template #actions>
<div class="ml-auto">
<v-btn color="primary" small to="/admin/manage/users">
<v-icon left>{{ $globals.icons.user }}</v-icon>
{{ $t("user.manage-users") }}
</v-btn>
</div>
</template>
</BaseStatCard>
</v-col>
<v-col cols="12" sm="12" md="4">
<BaseStatCard :icon="$globals.icons.group">
<template #after-heading>
<div class="ml-auto text-right">
<h2 class="body-3 grey--text font-weight-light">
{{ $t("group.groups") }}
</h2>
<h3 class="display-2 font-weight-light text--primary">
<small> {{ statistics.totalGroups }}</small>
</h3>
</div>
</template>
<template #actions>
<div class="ml-auto">
<v-btn color="primary" small to="/admin/manage/groups">
<v-icon left>{{ $globals.icons.group }}</v-icon>
{{ $t("group.manage-groups") }}
</v-btn>
</div>
</template>
</BaseStatCard>
</v-col>
</v-row>
<v-row class="mt-10" align-content="stretch">
<v-col>
<AdminEventViewer
v-if="events"
:events="events.events"
:total="events.total"
@delete-all="deleteEvents"
@delete-item="deleteEvent"
/>
</v-col>
</v-row>
</v-container>
</template>
<script lang="ts">
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
import AdminEventViewer from "@/components/Domain/Admin/AdminEventViewer.vue";
import { useAdminApi, useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
export default defineComponent({
components: { AdminEventViewer },
layout: "admin",
setup() {
const api = useUserApi();
const adminApi = useAdminApi();
function getStatistics() {
const statistics = useAsync(async () => {
const { data } = await adminApi.about.statistics();
return data;
}, useAsyncKey());
return statistics;
}
function getEvents() {
const events = useAsync(async () => {
const { data } = await api.events.getEvents();
return data;
}, useAsyncKey());
return events;
}
async function refreshEvents() {
const { data } = await api.events.getEvents();
events.value = data;
}
async function deleteEvent(id: number) {
const { response } = await api.events.deleteEvent(id);
if (response && response.status === 200) {
refreshEvents();
}
}
async function deleteEvents() {
const { response } = await api.events.deleteEvents();
if (response && response.status === 200) {
refreshEvents();
}
}
const events = getEvents();
const statistics = getStatistics();
return { statistics, events, deleteEvents, deleteEvent };
},
head() {
return {
title: this.$t("sidebar.dashboard") as string,
};
},
});
</script>
<style scoped>
</style>

View File

@ -204,7 +204,7 @@
<RecipeCard
v-for="mealplan in plan.meals"
:key="mealplan.id"
:recipe-id="0"
:recipe-id="mealplan.recipe ? mealplan.recipe.id : null"
:image-height="125"
class="mb-2"
:route="mealplan.recipe ? true : false"

7
frontend/run.sh Normal file
View File

@ -0,0 +1,7 @@
# Production entry point for the frontend docker container
# Web Server
caddy start --config /app/Caddyfile
# Start Node Application
yarn start -p 3001

View File

@ -12,7 +12,7 @@ class GunicornConfig:
# Env Variables
self.host = os.getenv("HOST", "127.0.0.1")
self.port = os.getenv("PORT", "80")
self.port = os.getenv("PORT", "9000")
self.log_level: str = os.getenv("LOG_LEVEL", "info")
self.bind: str = os.getenv("BIND", None)
self.errorlog: str = os.getenv("ERROR_LOG", "-") or None

View File

@ -6,10 +6,8 @@ from mealie.core.config import get_app_settings
from mealie.core.root_logger import get_logger
from mealie.core.settings.static import APP_VERSION
from mealie.routes import backup_routes, router, utility_routes
from mealie.routes.about import about_router
from mealie.routes.handlers import register_debug_handler
from mealie.routes.media import media_router
from mealie.services.events import create_general_event
from mealie.services.scheduler import SchedulerRegistry, SchedulerService, tasks
logger = get_logger()
@ -56,7 +54,6 @@ def start_scheduler():
SchedulerService.start()
SchedulerRegistry.register_daily(
tasks.purge_events_database,
tasks.purge_group_registration,
tasks.auto_backup,
tasks.purge_password_reset_tokens,
@ -72,7 +69,6 @@ def start_scheduler():
def api_routers():
app.include_router(router)
app.include_router(media_router)
app.include_router(about_router)
app.include_router(backup_routes.router)
app.include_router(utility_routes.router)
@ -103,8 +99,6 @@ def system_startup():
)
)
create_general_event("Application Startup", f"Mealie API started on port {settings.API_PORT}")
def main():
uvicorn.run(

View File

@ -7,7 +7,6 @@ from mealie.repos.repository_factory import AllRepositories
from mealie.repos.seed.init_users import default_user_init
from mealie.repos.seed.seeders import IngredientFoodsSeeder, IngredientUnitsSeeder, MultiPurposeLabelSeeder
from mealie.schema.user.user import GroupBase
from mealie.services.events import create_general_event
from mealie.services.group_services.group_utils import create_new_group
logger = root_logger.get_logger("init_db")
@ -62,7 +61,6 @@ def main():
else:
logger.info("Database Doesn't Exists, Initializing...")
init_db(db)
create_general_event("Initialize Database", "Initialize database with default values", session)
if __name__ == "__main__":

View File

@ -1,7 +1,5 @@
from .event import *
from .group import *
from .labels import *
from .recipe.recipe import *
from .server import *
from .sign_up import *
from .users import *

View File

@ -1,18 +0,0 @@
from sqlalchemy import Column, DateTime, Integer, String
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from ._model_utils import auto_init
class Event(SqlAlchemyBase, BaseMixins):
__tablename__ = "events"
id = Column(Integer, primary_key=True)
title = Column(String)
text = Column(String)
time_stamp = Column(DateTime)
category = Column(String)
@auto_init()
def __init__(self, **_) -> None:
pass

View File

@ -7,7 +7,7 @@ from ..recipe.category import Category, cookbooks_to_categories
class CookBook(SqlAlchemyBase, BaseMixins):
__tablename__ = "cookbooks"
id = Column(Integer, primary_key=True)
id = Column(guid.GUID, primary_key=True, default=guid.GUID.generate)
position = Column(Integer, nullable=False, default=1)
name = Column(String, nullable=False)

View File

@ -9,7 +9,7 @@ from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID, auto_init
from ..group.invite_tokens import GroupInviteToken
from ..group.webhooks import GroupWebhooksModel
from ..recipe.category import Category, group2categories
from ..recipe.category import Category, group_to_categories
from ..server.task import ServerTaskModel
from .cookbook import CookBook
from .mealplan import GroupMealPlan
@ -21,7 +21,7 @@ class Group(SqlAlchemyBase, BaseMixins):
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
name = sa.Column(sa.String, index=True, nullable=False, unique=True)
users = orm.relationship("User", back_populates="group")
categories = orm.relationship(Category, secondary=group2categories, single_parent=True, uselist=True)
categories = orm.relationship(Category, secondary=group_to_categories, single_parent=True, uselist=True)
invite_tokens = orm.relationship(
GroupInviteToken, back_populates="group", cascade="all, delete-orphan", uselist=True

View File

@ -9,6 +9,8 @@ from .._model_utils import auto_init
class GroupPreferencesModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "group_preferences"
id = sa.Column(GUID, primary_key=True, default=GUID.generate)
group_id = sa.Column(GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="preferences")

View File

@ -1,4 +1,4 @@
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
from sqlalchemy import Boolean, Column, ForeignKey, String, orm
from .._model_base import BaseMixins, SqlAlchemyBase
from .._model_utils import GUID, auto_init
@ -6,7 +6,7 @@ from .._model_utils import GUID, auto_init
class GroupWebhooksModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "webhook_urls"
id = Column(Integer, primary_key=True)
id = Column(GUID, primary_key=True, default=GUID.generate)
group = orm.relationship("Group", back_populates="webhooks", single_parent=True)
group_id = Column(GUID, ForeignKey("groups.id"), index=True)

View File

@ -11,8 +11,8 @@ from .._model_utils.guid import GUID
logger = root_logger.get_logger()
group2categories = sa.Table(
"group2categories",
group_to_categories = sa.Table(
"group_to_categories",
SqlAlchemyBase.metadata,
sa.Column("group_id", GUID, sa.ForeignKey("groups.id")),
sa.Column("category_id", GUID, sa.ForeignKey("categories.id")),
@ -35,7 +35,7 @@ recipes_to_categories = sa.Table(
cookbooks_to_categories = sa.Table(
"cookbooks_to_categories",
SqlAlchemyBase.metadata,
sa.Column("cookbook_id", sa.Integer, sa.ForeignKey("cookbooks.id")),
sa.Column("cookbook_id", GUID, sa.ForeignKey("cookbooks.id")),
sa.Column("category_id", GUID, sa.ForeignKey("categories.id")),
)

View File

@ -27,12 +27,12 @@ plan_rules_to_tags = sa.Table(
class Tag(SqlAlchemyBase, BaseMixins):
__tablename__ = "tags"
__table_args__ = (sa.UniqueConstraint("slug", "group_id", name="tags_slug_group_id_key"),)
id = sa.Column(guid.GUID, primary_key=True, default=guid.GUID.generate)
# ID Relationships
group_id = sa.Column(guid.GUID, sa.ForeignKey("groups.id"), nullable=False, index=True)
group = orm.relationship("Group", back_populates="tags", foreign_keys=[group_id])
id = sa.Column(guid.GUID, primary_key=True, default=guid.GUID.generate)
name = sa.Column(sa.String, index=True, nullable=False)
slug = sa.Column(sa.String, index=True, nullable=False)
recipes = orm.relationship("RecipeModel", secondary=recipes_to_tags, back_populates="tags")

View File

@ -15,8 +15,8 @@ recipes_to_tools = Table(
class Tool(SqlAlchemyBase, BaseMixins):
__tablename__ = "tools"
id = Column(GUID, primary_key=True, default=GUID.generate)
__table_args__ = (UniqueConstraint("slug", "group_id", name="tools_slug_group_id_key"),)
id = Column(GUID, primary_key=True, default=GUID.generate)
# ID Relationships
group_id = Column(GUID, ForeignKey("groups.id"), nullable=False)

View File

@ -1,17 +0,0 @@
from sqlalchemy import Boolean, Column, Integer, String
from mealie.db.models._model_base import BaseMixins, SqlAlchemyBase
from ._model_utils import auto_init
class SignUp(SqlAlchemyBase, BaseMixins):
__tablename__ = "sign_ups"
id = Column(Integer, primary_key=True)
token = Column(String, nullable=False, index=True)
name = Column(String, index=True)
admin = Column(Boolean, default=False)
@auto_init()
def __init__(self, **_) -> None:
pass

View File

@ -2,7 +2,6 @@ from functools import cached_property
from sqlalchemy.orm import Session
from mealie.db.models.event import Event
from mealie.db.models.group import Group, GroupMealPlan, ReportEntryModel, ReportModel
from mealie.db.models.group.cookbook import CookBook
from mealie.db.models.group.events import GroupEventNotifierModel
@ -26,12 +25,10 @@ from mealie.db.models.recipe.shared import RecipeShareTokenModel
from mealie.db.models.recipe.tag import Tag
from mealie.db.models.recipe.tool import Tool
from mealie.db.models.server.task import ServerTaskModel
from mealie.db.models.sign_up import SignUp
from mealie.db.models.users import LongLiveToken, User
from mealie.db.models.users.password_reset import PasswordResetModel
from mealie.repos.repository_meal_plan_rules import RepositoryMealPlanRules
from mealie.schema.cookbook.cookbook import ReadCookBook
from mealie.schema.events import Event as EventSchema
from mealie.schema.group.group_events import GroupEventNotifierOut
from mealie.schema.group.group_exports import GroupDataExport
from mealie.schema.group.group_preferences import ReadGroupPreferences
@ -52,7 +49,7 @@ from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUni
from mealie.schema.recipe.recipe_share_token import RecipeShareToken
from mealie.schema.reports.reports import ReportEntryOut, ReportOut
from mealie.schema.server import ServerTask
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser, SignUpOut
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser
from mealie.schema.user.user_passwords import PrivatePasswordResetToken
from .repository_generic import RepositoryGeneric
@ -114,7 +111,6 @@ class AllRepositories:
@cached_property
def categories(self) -> RepositoryCategories:
# TODO: Fix Typing for Category Repository
return RepositoryCategories(self.session, PK_ID, Category, CategoryOut)
@cached_property
@ -125,17 +121,6 @@ class AllRepositories:
def recipe_share_tokens(self) -> RepositoryGeneric[RecipeShareToken, RecipeShareTokenModel]:
return RepositoryGeneric(self.session, PK_ID, RecipeShareTokenModel, RecipeShareToken)
# ================================================================
# Site
@cached_property
def sign_up(self) -> RepositoryGeneric[SignUpOut, SignUp]:
return RepositoryGeneric(self.session, PK_ID, SignUp, SignUpOut)
@cached_property
def events(self) -> RepositoryGeneric[EventSchema, Event]:
return RepositoryGeneric(self.session, PK_ID, Event, EventSchema)
# ================================================================
# User

View File

@ -1,7 +0,0 @@
from fastapi import APIRouter
from . import events
about_router = APIRouter(prefix="/api/about")
about_router.include_router(events.router, tags=["Events: CRUD"])

View File

@ -1,25 +0,0 @@
from mealie.routes._base.routers import AdminAPIRouter
from mealie.schema.events import EventsOut
from .._base import BaseAdminController, controller
router = AdminAPIRouter(prefix="/events")
@controller(router)
class EventsController(BaseAdminController):
@router.get("", response_model=EventsOut)
async def get_events(self):
"""Get event from the Database"""
return EventsOut(total=self.repos.events.count_all(), events=self.repos.events.get_all(order_by="time_stamp"))
@router.delete("")
async def delete_events(self):
"""Get event from the Database"""
self.repos.events.delete_all()
return {"message": "All events deleted"}
@router.delete("/{item_id}")
async def delete_event(self, item_id: int):
"""Delete event from the Database"""
return self.repos.events.delete(item_id)

View File

@ -1,7 +1,7 @@
from datetime import timedelta
from typing import Optional
from fastapi import APIRouter, BackgroundTasks, Depends, Form, Request, status
from fastapi import APIRouter, Depends, Form, status
from fastapi.exceptions import HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
@ -13,7 +13,6 @@ from mealie.core.security import authenticate_user
from mealie.db.db_setup import generate_session
from mealie.routes._base.routers import UserAPIRouter
from mealie.schema.user import PrivateUser
from mealie.services.events import create_user_event
public_router = APIRouter(tags=["Users: Authentication"])
user_router = UserAPIRouter(tags=["Users: Authentication"])
@ -50,8 +49,6 @@ class MealieAuthToken(BaseModel):
@public_router.post("/token")
def get_token(
background_tasks: BackgroundTasks,
request: Request,
data: CustomOAuth2Form = Depends(),
session: Session = Depends(generate_session),
):
@ -61,9 +58,6 @@ def get_token(
user: PrivateUser = authenticate_user(session, email, password)
if not user:
background_tasks.add_task(
create_user_event, "Failed Login", f"Username: {email}, Source IP: '{request.client.host}'"
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
headers={"WWW-Authenticate": "Bearer"},

View File

@ -1,8 +1,7 @@
import operator
import shutil
from pathlib import Path
from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, status
from fastapi import Depends, File, HTTPException, UploadFile, status
from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_dirs
@ -16,7 +15,6 @@ from mealie.schema.admin import AllBackups, BackupFile, CreateBackup, ImportJob
from mealie.schema.user.user import PrivateUser
from mealie.services.backups import imports
from mealie.services.backups.exports import backup_all
from mealie.services.events import create_backup_event
router = AdminAPIRouter(prefix="/api/backups", tags=["Backups"])
logger = get_logger()
@ -38,9 +36,7 @@ def available_imports():
@router.post("/export/database", status_code=status.HTTP_201_CREATED)
def export_database(
background_tasks: BackgroundTasks, data: CreateBackup, session: Session = Depends(generate_session)
):
def export_database(data: CreateBackup, session: Session = Depends(generate_session)):
"""Generates a backup of the recipe database in json format."""
try:
export_path = backup_all(
@ -52,9 +48,7 @@ def export_database(
export_groups=data.options.groups,
export_notifications=data.options.notifications,
)
background_tasks.add_task(
create_backup_event, "Database Backup", f"Manual Backup Created '{Path(export_path).name}'", session
)
return {"export_path": export_path}
except Exception as e:
logger.error(e)
@ -83,15 +77,13 @@ async def download_backup_file(file_name: str):
@router.post("/{file_name}/import", status_code=status.HTTP_200_OK)
def import_database(
background_tasks: BackgroundTasks,
file_name: str,
import_data: ImportJob,
session: Session = Depends(generate_session),
user: PrivateUser = Depends(get_current_user),
):
"""Import a database backup file generated from Mealie."""
db_import = imports.import_database(
return imports.import_database(
user=user,
session=session,
archive=import_data.name,
@ -103,9 +95,6 @@ def import_database(
rebase=import_data.rebase,
)
background_tasks.add_task(create_backup_event, "Database Restore", f"Restore File: {file_name}", session)
return db_import
@router.delete("/{file_name}/delete", status_code=status.HTTP_200_OK)
def delete_backup(file_name: str):

View File

@ -2,6 +2,7 @@ from functools import cached_property
from typing import Type
from fastapi import APIRouter
from pydantic import UUID4
from mealie.core.exceptions import mealie_registered_exceptions
from mealie.routes._base import BaseUserController, controller
@ -54,17 +55,16 @@ class GroupCookbookController(BaseUserController):
return updated
@router.get("/{item_id}", response_model=RecipeCookBook)
def get_one(self, item_id: str):
try:
item_id = int(item_id)
return self.mixins.get_one(item_id)
except Exception:
def get_one(self, item_id: UUID4 | str):
if isinstance(item_id, str):
self.mixins.get_one(item_id, key="slug")
else:
return self.mixins.get_one(item_id)
@router.put("/{item_id}", response_model=RecipeCookBook)
def update_one(self, item_id: int, data: CreateCookBook):
def update_one(self, item_id: str, data: CreateCookBook):
return self.mixins.update_one(data, item_id)
@router.delete("/{item_id}", response_model=RecipeCookBook)
def delete_one(self, item_id: int):
def delete_one(self, item_id: str):
return self.mixins.delete_one(item_id)

View File

@ -1,6 +1,7 @@
from functools import cached_property
from fastapi import APIRouter, Depends
from pydantic import UUID4
from mealie.routes._base.abc_controller import BaseUserController
from mealie.routes._base.controller import controller
@ -32,13 +33,13 @@ class ReadWebhookController(BaseUserController):
return self.mixins.create_one(save)
@router.get("/{item_id}", response_model=ReadWebhook)
def get_one(self, item_id: int):
def get_one(self, item_id: UUID4):
return self.mixins.get_one(item_id)
@router.put("/{item_id}", response_model=ReadWebhook)
def update_one(self, item_id: int, data: CreateWebhook):
def update_one(self, item_id: UUID4, data: CreateWebhook):
return self.mixins.update_one(data, item_id)
@router.delete("/{item_id}", response_model=ReadWebhook)
def delete_one(self, item_id: int):
def delete_one(self, item_id: UUID4):
return self.mixins.delete_one(item_id) # type: ignore

View File

@ -48,9 +48,6 @@ else
add_user
init
# Web Server
caddy start --config /app/Caddyfile
# Start API
# uvicorn mealie.app:app --host 0.0.0.0 --port 9000
gunicorn mealie.app:app -b 0.0.0.0:9000 -k uvicorn.workers.UvicornWorker -c /app/gunicorn_conf.py --preload

View File

@ -28,7 +28,7 @@ class SaveCookBook(CreateCookBook):
class UpdateCookBook(SaveCookBook):
id: int
id: UUID4
class ReadCookBook(UpdateCookBook):

View File

@ -1,2 +0,0 @@
# GENERATED CODE - DO NOT MODIFY BY HAND
from .events import *

View File

@ -1,37 +0,0 @@
from datetime import datetime
from enum import Enum
from typing import Optional
from fastapi_camelcase import CamelModel
from pydantic import Field
class EventCategory(str, Enum):
general = "general"
recipe = "recipe"
backup = "backup"
scheduled = "scheduled"
migration = "migration"
group = "group"
user = "user"
class Event(CamelModel):
id: Optional[int]
title: str
text: str
time_stamp: datetime = Field(default_factory=datetime.now)
category: EventCategory = EventCategory.general
class Config:
orm_mode = True
class EventsOut(CamelModel):
total: int
events: list[Event]
class TestEvent(CamelModel):
id: Optional[int]
test_url: Optional[str]

View File

@ -1,6 +1,7 @@
from uuid import UUID
from fastapi_camelcase import CamelModel
from pydantic import UUID4
class UpdateGroupPreferences(CamelModel):
@ -21,7 +22,7 @@ class CreateGroupPreferences(UpdateGroupPreferences):
class ReadGroupPreferences(CreateGroupPreferences):
id: int
id: UUID4
class Config:
orm_mode = True

View File

@ -1,6 +1,7 @@
from uuid import UUID
from fastapi_camelcase import CamelModel
from pydantic import UUID4
class CreateWebhook(CamelModel):
@ -15,7 +16,7 @@ class SaveWebhook(CreateWebhook):
class ReadWebhook(SaveWebhook):
id: int
id: UUID4
class Config:
orm_mode = True

View File

@ -1,6 +1,5 @@
# GENERATED CODE - DO NOT MODIFY BY HAND
from .auth import *
from .registration import *
from .sign_up import *
from .user import *
from .user_passwords import *

View File

@ -1,17 +0,0 @@
from fastapi_camelcase import CamelModel
class SignUpIn(CamelModel):
name: str
admin: bool
class SignUpToken(SignUpIn):
token: str
class SignUpOut(SignUpToken):
id: int
class Config:
orm_mode = True

View File

@ -1,37 +0,0 @@
from sqlalchemy.orm.session import Session
from mealie.db.db_setup import create_session
from mealie.repos.all_repositories import get_repositories
from mealie.schema.events import Event, EventCategory
def save_event(title, text, category, session: Session):
event = Event(title=title, text=text, category=category)
session = session or create_session()
db = get_repositories(session)
db.events.create(event.dict())
def create_general_event(title, text, session=None):
category = EventCategory.general
save_event(title=title, text=text, category=category, session=session)
def create_recipe_event(title, text, session=None, **_):
category = EventCategory.recipe
save_event(title=title, text=text, category=category, session=session)
def create_backup_event(title, text, session=None):
category = EventCategory.backup
save_event(title=title, text=text, category=category, session=session)
def create_group_event(title, text, session=None):
category = EventCategory.group
save_event(title=title, text=text, category=category, session=session)
def create_user_event(title, text, session=None):
category = EventCategory.user
save_event(title=title, text=text, category=category, session=session)

View File

@ -1,5 +1,4 @@
from .auto_backup import *
from .purge_events import *
from .purge_group_exports import *
from .purge_password_reset import *
from .purge_registration import *

View File

@ -4,7 +4,6 @@ from mealie.core.config import get_app_dirs
app_dirs = get_app_dirs()
from mealie.db.db_setup import create_session
from mealie.services.backups.exports import backup_all
from mealie.services.events import create_backup_event
logger = root_logger.get_logger()
@ -17,6 +16,5 @@ def auto_backup():
session = create_session()
backup_all(session=session, tag="Auto", templates=templates)
logger.info("generating automated backup")
create_backup_event("Automated Backup", "Automated backup created", session)
session.close()
logger.info("automated backup generated")

View File

@ -1,19 +0,0 @@
import datetime
from mealie.core import root_logger
from mealie.db.db_setup import create_session
from mealie.db.models.event import Event
logger = root_logger.get_logger()
def purge_events_database():
"""Purges all events after 100"""
logger.info("purging events in database")
expiration_days = 7
limit = datetime.datetime.now() - datetime.timedelta(days=expiration_days)
session = create_session()
session.query(Event).filter(Event.time_stamp <= limit).delete()
session.commit()
session.close()
logger.info("events purges")

View File

@ -4,9 +4,11 @@ from uuid import UUID
import pytest
from fastapi.testclient import TestClient
from pydantic import UUID4
from mealie.repos.repository_factory import AllRepositories
from mealie.schema.cookbook.cookbook import ReadCookBook, SaveCookBook
from tests import utils
from tests.utils.assertion_helpers import assert_ignore_keys
from tests.utils.factories import random_string
from tests.utils.fixture_schemas import TestUser
@ -33,7 +35,7 @@ def get_page_data(group_id: UUID):
@dataclass
class TestCookbook:
id: int
id: UUID4
slug: str
name: str
data: dict
@ -41,7 +43,6 @@ class TestCookbook:
@pytest.fixture(scope="function")
def cookbooks(database: AllRepositories, unique_user: TestUser) -> list[TestCookbook]:
data: list[ReadCookBook] = []
yield_data: list[TestCookbook] = []
for _ in range(3):
@ -66,21 +67,28 @@ def test_create_cookbook(api_client: TestClient, unique_user: TestUser):
def test_read_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
sample = random.choice(cookbooks)
response = api_client.get(Routes.item(sample.id), headers=unique_user.token)
assert response.status_code == 200
assert_ignore_keys(response.json(), sample.data)
def test_update_cookbook(api_client: TestClient, unique_user: TestUser):
page_data = get_page_data(unique_user.group_id)
def test_update_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
cookbook = random.choice(cookbooks)
page_data["id"] = 1
page_data["name"] = "My New Name"
update_data = get_page_data(unique_user.group_id)
response = api_client.put(Routes.item(1), json=page_data, headers=unique_user.token)
update_data["name"] = random_string(10)
response = api_client.put(Routes.item(cookbook.id), json=update_data, headers=unique_user.token)
assert response.status_code == 200
response = api_client.get(Routes.item(cookbook.id), headers=unique_user.token)
assert response.status_code == 200
page_data = response.json()
assert page_data["name"] == update_data["name"]
assert page_data["slug"] == update_data["name"]
def test_update_cookbooks_many(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
pages = [x.data for x in cookbooks]
@ -90,7 +98,7 @@ def test_update_cookbooks_many(api_client: TestClient, unique_user: TestUser, co
page["position"] = x
page["group_id"] = str(unique_user.group_id)
response = api_client.put(Routes.base, json=reverse_order, headers=unique_user.token)
response = api_client.put(Routes.base, json=utils.jsonify(reverse_order), headers=unique_user.token)
assert response.status_code == 200
response = api_client.get(Routes.base, headers=unique_user.token)
@ -101,7 +109,7 @@ def test_update_cookbooks_many(api_client: TestClient, unique_user: TestUser, co
server_ids = [x["id"] for x in response.json()]
for know in known_ids: # Hacky check, because other tests don't cleanup after themselves :(
assert know in server_ids
assert str(know) in server_ids
def test_delete_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):

View File

@ -24,7 +24,9 @@ def test_create_webhook(api_client: TestClient, unique_user: TestUser, webhook_d
def test_read_webhook(api_client: TestClient, unique_user: TestUser, webhook_data):
response = api_client.post(Routes.base, json=webhook_data, headers=unique_user.token)
response = api_client.get(Routes.item(1), headers=unique_user.token)
id = response.json()["id"]
response = api_client.get(Routes.item(id), headers=unique_user.token)
webhook = response.json()
@ -36,13 +38,15 @@ def test_read_webhook(api_client: TestClient, unique_user: TestUser, webhook_dat
def test_update_webhook(api_client: TestClient, webhook_data, unique_user: TestUser):
webhook_data["id"] = 1
response = api_client.post(Routes.base, json=webhook_data, headers=unique_user.token)
id = response.json()["id"]
webhook_data["name"] = "My New Name"
webhook_data["url"] = "https://my-new-fake-url.com"
webhook_data["time"] = "01:00"
webhook_data["enabled"] = False
response = api_client.put(Routes.item(1), json=webhook_data, headers=unique_user.token)
response = api_client.put(Routes.item(id), json=webhook_data, headers=unique_user.token)
assert response.status_code == 200
@ -55,10 +59,13 @@ def test_update_webhook(api_client: TestClient, webhook_data, unique_user: TestU
assert response.status_code == 200
def test_delete_webhook(api_client: TestClient, unique_user: TestUser):
response = api_client.delete(Routes.item(1), headers=unique_user.token)
def test_delete_webhook(api_client: TestClient, webhook_data, unique_user: TestUser):
response = api_client.post(Routes.base, json=webhook_data, headers=unique_user.token)
id = response.json()["id"]
response = api_client.delete(Routes.item(id), headers=unique_user.token)
assert response.status_code == 200
response = api_client.get(Routes.item(1), headers=unique_user.token)
response = api_client.get(Routes.item(id), headers=unique_user.token)
assert response.status_code == 404