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
35
Caddyfile
@ -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
|
||||
}
|
||||
|
||||
}
|
19
Dockerfile
@ -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}
|
||||
|
||||
|
@ -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
|
||||
|
Before Width: | Height: | Size: 11 MiB |
Before Width: | Height: | Size: 14 MiB |
Before Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 2.9 MiB |
Before Width: | Height: | Size: 2.1 MiB |
Before Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 5.4 MiB |
Before Width: | Height: | Size: 4.6 MiB |
Before Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 334 KiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 187 KiB |
Before Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 619 KiB |
0
docs/docs/assets/img/home_screenshot.png
Executable file → Normal file
Before Width: | Height: | Size: 533 KiB After Width: | Height: | Size: 533 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 71 KiB |
@ -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
|
||||
|
||||

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

|
||||
|
||||
# Five Spice Popcorn Chicken
|
||||
It’s 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.
|
@ -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.
|
||||

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

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

|
||||
|
||||
|
||||
## 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.
|
@ -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
|
||||
```
|
@ -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 |
|
||||
|
||||

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

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

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

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

|
||||
|
@ -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
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
```
|
@ -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
|
||||
|
@ -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)
|
@ -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.
|
||||
|
||||

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

|
||||
|
||||
## 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
|
||||
|
||||
{: 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.
|
||||
|
||||

|
||||
|
||||
## 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": []
|
||||
}
|
||||
```
|
@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
#### Discord
|
||||

|
||||
|
||||
#### Gotify
|
||||

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

|
||||
|
||||
## Bulk Organize
|
||||
|
||||
The 'Organize' tab can be used to show all of the items that do not have any category or tag assigned.
|
||||
|
||||

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

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

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

|
||||
|
||||
|
||||
!!! warning
|
||||
API keys are stored in plain text in the database.
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
*/
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
|
@ -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>
|
@ -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
@ -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
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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__":
|
||||
|
@ -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 *
|
||||
|
@ -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
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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")),
|
||||
)
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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
|
||||
|
||||
|
@ -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"])
|
@ -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)
|
@ -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"},
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -28,7 +28,7 @@ class SaveCookBook(CreateCookBook):
|
||||
|
||||
|
||||
class UpdateCookBook(SaveCookBook):
|
||||
id: int
|
||||
id: UUID4
|
||||
|
||||
|
||||
class ReadCookBook(UpdateCookBook):
|
||||
|
@ -1,2 +0,0 @@
|
||||
# GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
from .events import *
|
@ -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]
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 *
|
||||
|
@ -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
|
@ -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)
|
@ -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 *
|
||||
|
@ -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")
|
||||
|
@ -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")
|
@ -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]):
|
||||
|
@ -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
|
||||
|