mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-25 09:13:11 -05:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65c35adc9d | ||
|
|
83b4846f0c | ||
|
|
6bc7ada20a | ||
|
|
8ce6f9038a | ||
|
|
e3c6d4c66c | ||
|
|
381a698220 | ||
|
|
c866557d58 | ||
|
|
bb5da2cb54 | ||
|
|
0fed5f54f6 | ||
|
|
f4bde93960 | ||
|
|
62300deea0 | ||
|
|
87f4b23711 | ||
|
|
8983745106 | ||
|
|
8872fd52cd | ||
|
|
b81b97d934 | ||
|
|
f798fafb3e | ||
|
|
dbbbe06a23 | ||
|
|
4b9eb5077a | ||
|
|
ff6db2374d | ||
|
|
3e69ea94d5 | ||
|
|
2e114cfa69 | ||
|
|
eb34ef0156 | ||
|
|
446755f678 | ||
|
|
08fe2d32b0 | ||
|
|
fb653ee2f6 | ||
|
|
a326a8c717 | ||
|
|
6e7cb5fb86 | ||
|
|
9289bd8e05 | ||
|
|
985b5634b7 | ||
|
|
2b2bc041bd | ||
|
|
6e16d4cc91 |
26
.github/workflows/scheduled-checks.yml
vendored
26
.github/workflows/scheduled-checks.yml
vendored
@@ -15,8 +15,30 @@ jobs:
|
|||||||
- name: Checkout 🛎
|
- name: Checkout 🛎
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Update pre-commit Hooks
|
- name: Setup Python
|
||||||
uses: vrslev/pre-commit-autoupdate@v1.0.0
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
|
||||||
|
- name: Set PY
|
||||||
|
shell: bash
|
||||||
|
run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/pre-commit
|
||||||
|
~/.cache/pip
|
||||||
|
key: pre-commit-${{ env.PY }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
|
||||||
|
- name: Install pre-commit
|
||||||
|
shell: bash
|
||||||
|
run: pip install -U pre-commit
|
||||||
|
|
||||||
|
- name: Run `pre-commit autoupdate`
|
||||||
|
shell: bash
|
||||||
|
run: pre-commit autoupdate --color=always
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v6
|
uses: peter-evans/create-pull-request@v6
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ repos:
|
|||||||
exclude: ^tests/data/
|
exclude: ^tests/data/
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.7.1
|
rev: v0.7.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
### General
|
### General
|
||||||
|
|
||||||
| Variables | Default | Description |
|
| Variables | Default | Description |
|
||||||
| ----------------------------- | :-------------------: | --------------------------------------------------------------------------------------------------------- |
|
| ----------------------------- | :-------------------: | -------------------------------------------------------------------------------------------------- |
|
||||||
| PUID | 911 | UserID permissions between host OS and container |
|
| PUID | 911 | UserID permissions between host OS and container |
|
||||||
| PGID | 911 | GroupID permissions between host OS and container |
|
| PGID | 911 | GroupID permissions between host OS and container |
|
||||||
| DEFAULT_GROUP | Home | The default group for users |
|
| DEFAULT_GROUP | Home | The default group for users |
|
||||||
@@ -58,9 +58,18 @@
|
|||||||
Changing the webworker settings may cause unforeseen memory leak issues with Mealie. It's best to leave these at the defaults unless you begin to experience issues with multiple users. Exercise caution when changing these settings
|
Changing the webworker settings may cause unforeseen memory leak issues with Mealie. It's best to leave these at the defaults unless you begin to experience issues with multiple users. Exercise caution when changing these settings
|
||||||
|
|
||||||
| Variables | Default | Description |
|
| Variables | Default | Description |
|
||||||
| --------------- | :-----: | ----------------------------------------------------------------------------- |
|
| --------------- | :-----: | -------------------------------------------------------------------------------- |
|
||||||
| UVICORN_WORKERS | 1 | Sets the number of workers for the web server. [More info here][unicorn_workers] |
|
| UVICORN_WORKERS | 1 | Sets the number of workers for the web server. [More info here][unicorn_workers] |
|
||||||
|
|
||||||
|
### TLS
|
||||||
|
|
||||||
|
Use this only when mealie is run without a webserver or reverse proxy.
|
||||||
|
|
||||||
|
| Variables | Default | Description |
|
||||||
|
| -------------------- | :-----: | ------------------------ |
|
||||||
|
| TLS_CERTIFICATE_PATH | None | File path to Certificate |
|
||||||
|
| TLS_PRIVATE_KEY_PATH | None | File path to private key |
|
||||||
|
|
||||||
### LDAP
|
### LDAP
|
||||||
|
|
||||||
| Variables | Default | Description |
|
| Variables | Default | Description |
|
||||||
@@ -86,12 +95,12 @@ Changing the webworker settings may cause unforeseen memory leak issues with Mea
|
|||||||
For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
|
For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
|
||||||
|
|
||||||
| Variables | Default | Description |
|
| Variables | Default | Description |
|
||||||
| ---------------------- | :-----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ------------------------------------------------- | :-----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| OIDC_AUTH_ENABLED | False | Enables authentication via OpenID Connect |
|
| OIDC_AUTH_ENABLED | False | Enables authentication via OpenID Connect |
|
||||||
| OIDC_SIGNUP_ENABLED | True | Enables new users to be created when signing in for the first time with OIDC |
|
| OIDC_SIGNUP_ENABLED | True | Enables new users to be created when signing in for the first time with OIDC |
|
||||||
| OIDC_CONFIGURATION_URL | None | The URL to the OIDC configuration of your provider. This is usually something like https://auth.example.com/.well-known/openid-configuration |
|
| OIDC_CONFIGURATION_URL | None | The URL to the OIDC configuration of your provider. This is usually something like https://auth.example.com/.well-known/openid-configuration |
|
||||||
| OIDC_CLIENT_ID | None | The client id of your configured client in your provider |
|
| OIDC_CLIENT_ID | None | The client id of your configured client in your provider |
|
||||||
| OIDC_CLIENT_SECRET <br/> :octicons-tag-24: v2.0.0 | None | The client secret of your configured client in your provider|
|
| OIDC_CLIENT_SECRET <br/> :octicons-tag-24: v2.0.0 | None | The client secret of your configured client in your provider |
|
||||||
| OIDC_USER_GROUP | None | If specified, only users belonging to this group will be able to successfully authenticate. For more information see [this page](../authentication/oidc-v2.md#groups) |
|
| OIDC_USER_GROUP | None | If specified, only users belonging to this group will be able to successfully authenticate. For more information see [this page](../authentication/oidc-v2.md#groups) |
|
||||||
| OIDC_ADMIN_GROUP | None | If specified, users belonging to this group will be able to successfully authenticate *and* be made an admin. For more information see [this page](../authentication/oidc-v2.md#groups) |
|
| OIDC_ADMIN_GROUP | None | If specified, users belonging to this group will be able to successfully authenticate *and* be made an admin. For more information see [this page](../authentication/oidc-v2.md#groups) |
|
||||||
| OIDC_AUTO_REDIRECT | False | If `True`, then the login page will be bypassed and you will be sent directly to your Identity Provider. You can still get to the login page by adding `?direct=1` to the login URL |
|
| OIDC_AUTO_REDIRECT | False | If `True`, then the login page will be bypassed and you will be sent directly to your Identity Provider. You can still get to the login page by adding `?direct=1` to the login URL |
|
||||||
@@ -99,6 +108,7 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
|
|||||||
| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked |
|
| OIDC_REMEMBER_ME | False | Because redirects bypass the login screen, you cant extend your session by clicking the "Remember Me" checkbox. By setting this value to true, a session will be extended as if "Remember Me" was checked |
|
||||||
| OIDC_USER_CLAIM | email | This is the claim which Mealie will use to look up an existing user by (e.g. "email", "preferred_username") |
|
| OIDC_USER_CLAIM | email | This is the claim which Mealie will use to look up an existing user by (e.g. "email", "preferred_username") |
|
||||||
| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** |
|
| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** |
|
||||||
|
| OIDC_SCOPES_OVERRIDE | None | Advanced configuration used to override the scopes requested from the IdP. **Most users won't need to change this**. At a minimum, 'openid profile email' are required. |
|
||||||
| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
|
| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
|
||||||
|
|
||||||
### OpenAI
|
### OpenAI
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
|
|||||||
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
|
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
|
||||||
|
|
||||||
1. Take a backup just in case!
|
1. Take a backup just in case!
|
||||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v2.0.0`
|
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v2.1.0`
|
||||||
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
|
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
|
||||||
4. Restart the container
|
4. Restart the container
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
|||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mealie:
|
mealie:
|
||||||
image: ghcr.io/mealie-recipes/mealie:v2.0.0 # (3)
|
image: ghcr.io/mealie-recipes/mealie:v2.1.0 # (3)
|
||||||
container_name: mealie
|
container_name: mealie
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
|||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mealie:
|
mealie:
|
||||||
image: ghcr.io/mealie-recipes/mealie:v2.0.0 # (3)
|
image: ghcr.io/mealie-recipes/mealie:v2.1.0 # (3)
|
||||||
container_name: mealie
|
container_name: mealie
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@
|
|||||||
width="100%"
|
width="100%"
|
||||||
max-width="1100px"
|
max-width="1100px"
|
||||||
:icon="$globals.icons.pages"
|
:icon="$globals.icons.pages"
|
||||||
:title="$t('general.edit')"
|
:title="$tc('general.edit')"
|
||||||
:submit-icon="$globals.icons.save"
|
:submit-icon="$globals.icons.save"
|
||||||
:submit-text="$tc('general.save')"
|
:submit-text="$tc('general.save')"
|
||||||
:submit-disabled="!editTarget.queryFilterString"
|
:submit-disabled="!editTarget.queryFilterString"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<v-toolbar-title class="headline"> {{ book.name }} </v-toolbar-title>
|
<v-toolbar-title class="headline"> {{ book.name }} </v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="isOwnGroup"
|
v-if="canEdit"
|
||||||
class="mx-1"
|
class="mx-1"
|
||||||
:edit="true"
|
:edit="true"
|
||||||
@click="handleEditCookbook"
|
@click="handleEditCookbook"
|
||||||
@@ -79,6 +79,15 @@
|
|||||||
const tab = ref(null);
|
const tab = ref(null);
|
||||||
const book = getOne(slug);
|
const book = getOne(slug);
|
||||||
|
|
||||||
|
const isOwnHousehold = computed(() => {
|
||||||
|
if (!($auth.user && book.value?.householdId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $auth.user.householdId === book.value.householdId;
|
||||||
|
})
|
||||||
|
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||||
|
|
||||||
const dialogStates = reactive({
|
const dialogStates = reactive({
|
||||||
edit: false,
|
edit: false,
|
||||||
});
|
});
|
||||||
@@ -118,7 +127,7 @@
|
|||||||
recipes,
|
recipes,
|
||||||
removeRecipe,
|
removeRecipe,
|
||||||
replaceRecipes,
|
replaceRecipes,
|
||||||
isOwnGroup,
|
canEdit,
|
||||||
dialogStates,
|
dialogStates,
|
||||||
editTarget,
|
editTarget,
|
||||||
handleEditCookbook,
|
handleEditCookbook,
|
||||||
|
|||||||
@@ -96,7 +96,13 @@ import RecipePageTitleContent from "./RecipePageParts/RecipePageTitleContent.vue
|
|||||||
import RecipePageComments from "./RecipePageParts/RecipePageComments.vue";
|
import RecipePageComments from "./RecipePageParts/RecipePageComments.vue";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import RecipePrintContainer from "~/components/Domain/Recipe/RecipePrintContainer.vue";
|
import RecipePrintContainer from "~/components/Domain/Recipe/RecipePrintContainer.vue";
|
||||||
import { EditorMode, PageMode, usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
import {
|
||||||
|
clearPageState,
|
||||||
|
EditorMode,
|
||||||
|
PageMode,
|
||||||
|
usePageState,
|
||||||
|
usePageUser,
|
||||||
|
} from "~/composables/recipe-page/shared-state";
|
||||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
import { Recipe } from "~/lib/api/types/recipe";
|
import { Recipe } from "~/lib/api/types/recipe";
|
||||||
import { useRouteQuery } from "~/composables/use-router";
|
import { useRouteQuery } from "~/composables/use-router";
|
||||||
@@ -170,6 +176,9 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
deactivateNavigationWarning();
|
deactivateNavigationWarning();
|
||||||
|
|
||||||
|
clearPageState(props.recipe.slug || "");
|
||||||
|
console.debug("reset RecipePage state during unmount");
|
||||||
});
|
});
|
||||||
|
|
||||||
/** =============================================================
|
/** =============================================================
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, onUnmounted } from "@nuxtjs/composition-api";
|
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||||
import { clearPageState, usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
||||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
import { Recipe } from "~/lib/api/types/recipe";
|
import { Recipe } from "~/lib/api/types/recipe";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
@@ -75,10 +75,6 @@ export default defineComponent({
|
|||||||
return households.value.find((h) => h.id === owner.householdId);
|
return households.value.find((h) => h.id === owner.householdId);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearPageState(props.recipe.slug);
|
|
||||||
console.debug("reset RecipePage state during unmount");
|
|
||||||
});
|
|
||||||
async function uploadImage(fileObject: File) {
|
async function uploadImage(fileObject: File) {
|
||||||
if (!props.recipe || !props.recipe.slug) {
|
if (!props.recipe || !props.recipe.slug) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
<tr v-for="(value, key) in recipe.nutrition" :key="key">
|
<tr v-for="(value, key) in recipe.nutrition" :key="key">
|
||||||
<template v-if="value">
|
<template v-if="value">
|
||||||
<td>{{ labels[key].label }}</td>
|
<td>{{ labels[key].label }}</td>
|
||||||
<td>{{ value || '-' }}</td>
|
<td>{{ value ? (labels[key].suffix ? `${value} ${labels[key].suffix}` : value) : '-' }}</td>
|
||||||
</template>
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -322,10 +322,32 @@ li {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nutrition-table {
|
.nutrition-table {
|
||||||
width: 25%;
|
max-width: 80%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nutrition-table th,
|
||||||
|
.nutrition-table td {
|
||||||
|
padding: 6px 10px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nutrition-table th {
|
||||||
|
font-weight: bold;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nutrition-table td:first-child {
|
||||||
|
width: 70%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nutrition-table td:last-child {
|
||||||
|
width: 30%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
.nutrition-table td {
|
.nutrition-table td {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@@ -82,12 +82,17 @@ import { computed, defineComponent, onMounted, ref, useContext, useRoute } from
|
|||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import AppHeader from "@/components/Layout/LayoutParts/AppHeader.vue";
|
import AppHeader from "@/components/Layout/LayoutParts/AppHeader.vue";
|
||||||
import AppSidebar from "@/components/Layout/LayoutParts/AppSidebar.vue";
|
import AppSidebar from "@/components/Layout/LayoutParts/AppSidebar.vue";
|
||||||
import { SidebarLinks } from "~/types/application-types";
|
import { SideBarLink } from "~/types/application-types";
|
||||||
import LanguageDialog from "~/components/global/LanguageDialog.vue";
|
import LanguageDialog from "~/components/global/LanguageDialog.vue";
|
||||||
import TheSnackbar from "@/components/Layout/LayoutParts/TheSnackbar.vue";
|
import TheSnackbar from "@/components/Layout/LayoutParts/TheSnackbar.vue";
|
||||||
import { useAppInfo } from "~/composables/api";
|
import { useAppInfo } from "~/composables/api";
|
||||||
import { useCookbooks, usePublicCookbooks } from "~/composables/use-group-cookbooks";
|
import { useCookbooks, usePublicCookbooks } from "~/composables/use-group-cookbooks";
|
||||||
|
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
||||||
|
import { useHouseholdStore, usePublicHouseholdStore } from "~/composables/store/use-household-store";
|
||||||
import { useToggleDarkMode } from "~/composables/use-utils";
|
import { useToggleDarkMode } from "~/composables/use-utils";
|
||||||
|
import { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
|
import { HouseholdSummary } from "~/lib/api/types/household";
|
||||||
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { AppHeader, AppSidebar, LanguageDialog, TheSnackbar },
|
components: { AppHeader, AppSidebar, LanguageDialog, TheSnackbar },
|
||||||
@@ -99,6 +104,15 @@ export default defineComponent({
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||||
const { cookbooks } = isOwnGroup.value ? useCookbooks() : usePublicCookbooks(groupSlug.value || "");
|
const { cookbooks } = isOwnGroup.value ? useCookbooks() : usePublicCookbooks(groupSlug.value || "");
|
||||||
|
const cookbookPreferences = useCookbookPreferences();
|
||||||
|
const { store: households } = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value || "");
|
||||||
|
|
||||||
|
const householdsById = computed(() => {
|
||||||
|
return households.value.reduce((acc, household) => {
|
||||||
|
acc[household.id] = household;
|
||||||
|
return acc;
|
||||||
|
}, {} as { [key: string]: HouseholdSummary });
|
||||||
|
});
|
||||||
|
|
||||||
const appInfo = useAppInfo();
|
const appInfo = useAppInfo();
|
||||||
const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices);
|
const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices);
|
||||||
@@ -113,29 +127,57 @@ export default defineComponent({
|
|||||||
sidebar.value = !$vuetify.breakpoint.md;
|
sidebar.value = !$vuetify.breakpoint.md;
|
||||||
});
|
});
|
||||||
|
|
||||||
const cookbookLinks = computed(() => {
|
function cookbookAsLink(cookbook: ReadCookBook): SideBarLink {
|
||||||
if (!cookbooks.value) return [];
|
|
||||||
return cookbooks.value.map((cookbook) => {
|
|
||||||
return {
|
return {
|
||||||
key: cookbook.slug,
|
key: cookbook.slug || "",
|
||||||
icon: $globals.icons.pages,
|
icon: $globals.icons.pages,
|
||||||
title: cookbook.name,
|
title: cookbook.name,
|
||||||
to: `/g/${groupSlug.value}/cookbooks/${cookbook.slug as string}`,
|
to: `/g/${groupSlug.value}/cookbooks/${cookbook.slug || ""}`,
|
||||||
|
restricted: false,
|
||||||
};
|
};
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
interface Link {
|
|
||||||
insertDivider: boolean;
|
|
||||||
icon: string;
|
|
||||||
title: string;
|
|
||||||
subtitle: string | null;
|
|
||||||
to: string;
|
|
||||||
restricted: boolean;
|
|
||||||
hide: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const createLinks = computed<Link[]>(() => [
|
const currentUserHouseholdId = computed(() => $auth.user?.householdId);
|
||||||
|
const cookbookLinks = computed<SideBarLink[]>(() => {
|
||||||
|
if (!cookbooks.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
cookbooks.value.sort((a, b) => (a.position || 0) - (b.position || 0));
|
||||||
|
|
||||||
|
const ownLinks: SideBarLink[] = [];
|
||||||
|
const links: SideBarLink[] = [];
|
||||||
|
const cookbooksByHousehold = cookbooks.value.reduce((acc, cookbook) => {
|
||||||
|
const householdName = householdsById.value[cookbook.householdId]?.name || "";
|
||||||
|
if (!acc[householdName]) {
|
||||||
|
acc[householdName] = [];
|
||||||
|
}
|
||||||
|
acc[householdName].push(cookbook);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, ReadCookBook[]>);
|
||||||
|
|
||||||
|
Object.entries(cookbooksByHousehold).forEach(([householdName, cookbooks]) => {
|
||||||
|
if (cookbooks[0].householdId === currentUserHouseholdId.value) {
|
||||||
|
ownLinks.push(...cookbooks.map(cookbookAsLink));
|
||||||
|
} else {
|
||||||
|
links.push({
|
||||||
|
key: householdName,
|
||||||
|
icon: $globals.icons.book,
|
||||||
|
title: householdName,
|
||||||
|
children: cookbooks.map(cookbookAsLink),
|
||||||
|
restricted: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
links.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
if ($auth.user && cookbookPreferences.value.hideOtherHouseholds) {
|
||||||
|
return ownLinks;
|
||||||
|
} else {
|
||||||
|
return [...ownLinks, ...links];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const createLinks = computed<SideBarLink[]>(() => [
|
||||||
{
|
{
|
||||||
insertDivider: false,
|
insertDivider: false,
|
||||||
icon: $globals.icons.link,
|
icon: $globals.icons.link,
|
||||||
@@ -165,7 +207,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const bottomLinks = computed<SidebarLinks>(() => [
|
const bottomLinks = computed<SideBarLink[]>(() => [
|
||||||
{
|
{
|
||||||
icon: $globals.icons.cog,
|
icon: $globals.icons.cog,
|
||||||
title: i18n.tc("general.settings"),
|
title: i18n.tc("general.settings"),
|
||||||
@@ -174,7 +216,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const topLinks = computed<SidebarLinks>(() => [
|
const topLinks = computed<SideBarLink[]>(() => [
|
||||||
{
|
{
|
||||||
icon: $globals.icons.silverwareForkKnife,
|
icon: $globals.icons.silverwareForkKnife,
|
||||||
to: `/g/${groupSlug.value}`,
|
to: `/g/${groupSlug.value}`,
|
||||||
|
|||||||
@@ -135,7 +135,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, reactive, toRefs, useContext, watch } from "@nuxtjs/composition-api";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import { SidebarLinks } from "~/types/application-types";
|
import { SidebarLinks } from "~/types/application-types";
|
||||||
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||||
@@ -192,13 +192,29 @@ export default defineComponent({
|
|||||||
const userProfileLink = computed(() => $auth.user ? "/user/profile" : undefined);
|
const userProfileLink = computed(() => $auth.user ? "/user/profile" : undefined);
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
dropDowns: {},
|
dropDowns: {} as Record<string, boolean>,
|
||||||
topSelected: null as string[] | null,
|
topSelected: null as string[] | null,
|
||||||
secondarySelected: null as string[] | null,
|
secondarySelected: null as string[] | null,
|
||||||
bottomSelected: null as string[] | null,
|
bottomSelected: null as string[] | null,
|
||||||
hasOpenedBefore: false as boolean,
|
hasOpenedBefore: false as boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const allLinks = computed(() => [...props.topLink, ...(props.secondaryLinks || []), ...(props.bottomLinks || [])]);
|
||||||
|
function initDropdowns() {
|
||||||
|
allLinks.value.forEach((link) => {
|
||||||
|
state.dropDowns[link.title] = link.childrenStartExpanded || false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => allLinks,
|
||||||
|
() => {
|
||||||
|
initDropdowns();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
userFavoritesLink,
|
userFavoritesLink,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmo
|
|||||||
// casting to number is required as sometimes quantity is a string
|
// casting to number is required as sometimes quantity is a string
|
||||||
if (quantity && Number(quantity) !== 0) {
|
if (quantity && Number(quantity) !== 0) {
|
||||||
if (unit && !unit.fraction) {
|
if (unit && !unit.fraction) {
|
||||||
returnQty = (quantity * scale).toString();
|
returnQty = Number((quantity * scale).toPrecision(3)).toString();
|
||||||
} else {
|
} else {
|
||||||
const fraction = frac(quantity * scale, 10, true);
|
const fraction = frac(quantity * scale, 10, true);
|
||||||
if (fraction[0] !== undefined && fraction[0] > 0) {
|
if (fraction[0] !== undefined && fraction[0] > 0) {
|
||||||
|
|||||||
@@ -99,10 +99,10 @@ export const useCookbooks = function () {
|
|||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
},
|
},
|
||||||
async createOne() {
|
async createOne(name: string | null = null) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const { data } = await api.cookbooks.createOne({
|
const { data } = await api.cookbooks.createOne({
|
||||||
name: i18n.t("cookbook.household-cookbook-name", [household.value?.name || "", String((cookbookStore?.value?.length ?? 0) + 1)]) as string,
|
name: name || i18n.t("cookbook.household-cookbook-name", [household.value?.name || "", String((cookbookStore?.value?.length ?? 0) + 1)]) as string,
|
||||||
position: (cookbookStore?.value?.length ?? 0) + 1,
|
position: (cookbookStore?.value?.length ?? 0) + 1,
|
||||||
queryFilterString: "",
|
queryFilterString: "",
|
||||||
});
|
});
|
||||||
@@ -129,18 +129,18 @@ export const useCookbooks = function () {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateOrder() {
|
async updateOrder(cookbooks: ReadCookBook[]) {
|
||||||
if (!cookbookStore?.value) {
|
if (!cookbooks?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
cookbookStore.value.forEach((element, index) => {
|
cookbooks.forEach((element, index) => {
|
||||||
element.position = index + 1;
|
element.position = index + 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data } = await api.cookbooks.updateAll(cookbookStore.value);
|
const { data } = await api.cookbooks.updateAll(cookbooks);
|
||||||
|
|
||||||
if (data && cookbookStore?.value) {
|
if (data && cookbookStore?.value) {
|
||||||
this.refreshAll();
|
this.refreshAll();
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export interface UserParsingPreferences {
|
|||||||
parser: RegisteredParser;
|
parser: RegisteredParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserCookbooksPreferences {
|
||||||
|
hideOtherHouseholds: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> {
|
export function useUserMealPlanPreferences(): Ref<UserMealPlanPreferences> {
|
||||||
const fromStorage = useLocalStorage(
|
const fromStorage = useLocalStorage(
|
||||||
"meal-planner-preferences",
|
"meal-planner-preferences",
|
||||||
@@ -153,3 +157,17 @@ export function useParsingPreferences(): Ref<UserParsingPreferences> {
|
|||||||
|
|
||||||
return fromStorage;
|
return fromStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCookbookPreferences(): Ref<UserCookbooksPreferences> {
|
||||||
|
const fromStorage = useLocalStorage(
|
||||||
|
"cookbook-preferences",
|
||||||
|
{
|
||||||
|
hideOtherHouseholds: false,
|
||||||
|
},
|
||||||
|
{ mergeDefaults: true }
|
||||||
|
// we cast to a Ref because by default it will return an optional type ref
|
||||||
|
// but since we pass defaults we know all properties are set.
|
||||||
|
) as unknown as Ref<UserCookbooksPreferences>;
|
||||||
|
|
||||||
|
return fromStorage;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1327,6 +1327,8 @@
|
|||||||
"cookbook": {
|
"cookbook": {
|
||||||
"cookbooks": "Cookbooks",
|
"cookbooks": "Cookbooks",
|
||||||
"description": "Cookbooks are another way to organize recipes by creating cross sections of recipes, organizers, and other filters. Creating a cookbook will add an entry to the side-bar and all the recipes with the filters chosen will be displayed in the cookbook.",
|
"description": "Cookbooks are another way to organize recipes by creating cross sections of recipes, organizers, and other filters. Creating a cookbook will add an entry to the side-bar and all the recipes with the filters chosen will be displayed in the cookbook.",
|
||||||
|
"hide-cookbooks-from-other-households": "Hide Cookbooks from Other Households",
|
||||||
|
"hide-cookbooks-from-other-households-description": "When enabled, only cookbooks from your household will appear on the sidebar",
|
||||||
"public-cookbook": "Public Cookbook",
|
"public-cookbook": "Public Cookbook",
|
||||||
"public-cookbook-description": "Public Cookbooks can be shared with non-mealie users and will be displayed on your groups page.",
|
"public-cookbook-description": "Public Cookbooks can be shared with non-mealie users and will be displayed on your groups page.",
|
||||||
"filter-options": "Filter Options",
|
"filter-options": "Filter Options",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mealie",
|
"name": "mealie",
|
||||||
"version": "2.0.0",
|
"version": "2.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt",
|
"dev": "nuxt",
|
||||||
|
|||||||
@@ -48,20 +48,33 @@
|
|||||||
{{ $t('cookbook.description') }}
|
{{ $t('cookbook.description') }}
|
||||||
</BasePageTitle>
|
</BasePageTitle>
|
||||||
|
|
||||||
|
<div class="my-6">
|
||||||
|
<v-checkbox
|
||||||
|
v-model="cookbookPreferences.hideOtherHouseholds"
|
||||||
|
:label="$tc('cookbook.hide-cookbooks-from-other-households')"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<div class="ml-8">
|
||||||
|
<p class="text-subtitle-2 my-0 py-0">
|
||||||
|
{{ $tc("cookbook.hide-cookbooks-from-other-households-description") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Create New -->
|
<!-- Create New -->
|
||||||
<BaseButton create @click="createCookbook" />
|
<BaseButton create @click="createCookbook" />
|
||||||
|
|
||||||
<!-- Cookbook List -->
|
<!-- Cookbook List -->
|
||||||
<v-expansion-panels class="mt-2">
|
<v-expansion-panels class="mt-2">
|
||||||
<draggable
|
<draggable
|
||||||
v-model="cookbooks"
|
v-model="myCookbooks"
|
||||||
handle=".handle"
|
handle=".handle"
|
||||||
delay="250"
|
delay="250"
|
||||||
:delay-on-touch-only="true"
|
:delay-on-touch-only="true"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@change="actions.updateOrder()"
|
@change="actions.updateOrder(myCookbooks)"
|
||||||
>
|
>
|
||||||
<v-expansion-panel v-for="cookbook in cookbooks" :key="cookbook.id" class="my-2 left-border rounded">
|
<v-expansion-panel v-for="cookbook in myCookbooks" :key="cookbook.id" class="my-2 left-border rounded">
|
||||||
<v-expansion-panel-header disable-icon-rotate class="headline">
|
<v-expansion-panel-header disable-icon-rotate class="headline">
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
<v-icon large left>
|
<v-icon large left>
|
||||||
@@ -110,11 +123,13 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent, onBeforeUnmount, onMounted, reactive, ref } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, onBeforeUnmount, onMounted, reactive, ref, useContext } from "@nuxtjs/composition-api";
|
||||||
import draggable from "vuedraggable";
|
import draggable from "vuedraggable";
|
||||||
import { useCookbooks } from "@/composables/use-group-cookbooks";
|
import { useCookbooks } from "@/composables/use-group-cookbooks";
|
||||||
|
import { useHouseholdSelf } from "@/composables/use-households";
|
||||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||||
import { ReadCookBook } from "~/lib/api/types/cookbook";
|
import { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
|
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { CookbookEditor, draggable },
|
components: { CookbookEditor, draggable },
|
||||||
@@ -124,13 +139,28 @@ export default defineComponent({
|
|||||||
create: false,
|
create: false,
|
||||||
delete: false,
|
delete: false,
|
||||||
});
|
});
|
||||||
const { cookbooks, actions } = useCookbooks();
|
|
||||||
|
const { $auth, i18n } = useContext();
|
||||||
|
const { cookbooks: allCookbooks, actions } = useCookbooks();
|
||||||
|
const myCookbooks = computed<ReadCookBook[]>({
|
||||||
|
get: () => {
|
||||||
|
return allCookbooks.value?.filter((cookbook) => {
|
||||||
|
return cookbook.householdId === $auth.user?.householdId;
|
||||||
|
}) || [];
|
||||||
|
},
|
||||||
|
set: (value: ReadCookBook[]) => {
|
||||||
|
actions.updateOrder(value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { household } = useHouseholdSelf();
|
||||||
|
const cookbookPreferences = useCookbookPreferences()
|
||||||
|
|
||||||
// create
|
// create
|
||||||
const createTargetKey = ref(0);
|
const createTargetKey = ref(0);
|
||||||
const createTarget = ref<ReadCookBook | null>(null);
|
const createTarget = ref<ReadCookBook | null>(null);
|
||||||
async function createCookbook() {
|
async function createCookbook() {
|
||||||
await actions.createOne().then((cookbook) => {
|
const name = i18n.t("cookbook.household-cookbook-name", [household.value?.name || "", String((myCookbooks.value?.length ?? 0) + 1)]) as string
|
||||||
|
await actions.createOne(name).then((cookbook) => {
|
||||||
createTarget.value = cookbook as ReadCookBook;
|
createTarget.value = cookbook as ReadCookBook;
|
||||||
createTargetKey.value++;
|
createTargetKey.value++;
|
||||||
});
|
});
|
||||||
@@ -177,7 +207,8 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cookbooks,
|
myCookbooks,
|
||||||
|
cookbookPreferences,
|
||||||
actions,
|
actions,
|
||||||
dialogStates,
|
dialogStates,
|
||||||
// create
|
// create
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ export default defineComponent({
|
|||||||
// we explicitly set booleans to false since forms don't POST unchecked boxes
|
// we explicitly set booleans to false since forms don't POST unchecked boxes
|
||||||
const createTarget = ref<CreateIngredientUnit>({
|
const createTarget = ref<CreateIngredientUnit>({
|
||||||
name: "",
|
name: "",
|
||||||
fraction: false,
|
fraction: true,
|
||||||
useAbbreviation: false,
|
useAbbreviation: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
<!-- View By Label -->
|
<!-- View By Label -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div v-for="(value, key) in itemsByLabel" :key="key" class="mb-6">
|
<div v-for="(value, key) in itemsByLabel" :key="key" class="pb-4">
|
||||||
<v-btn
|
<v-btn
|
||||||
:color="getLabelColor(value[0]) ? getLabelColor(value[0]) : '#959595'"
|
:color="getLabelColor(value[0]) ? getLabelColor(value[0]) : '#959595'"
|
||||||
:style="{
|
:style="{
|
||||||
@@ -470,7 +470,7 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// =====================================
|
// =====================================
|
||||||
// Collapsables
|
// Collapsable Labels
|
||||||
const labelOpenState = ref<{ [key: string]: boolean }>({});
|
const labelOpenState = ref<{ [key: string]: boolean }>({});
|
||||||
|
|
||||||
const initializeLabelOpenStates = () => {
|
const initializeLabelOpenStates = () => {
|
||||||
@@ -480,8 +480,8 @@ export default defineComponent({
|
|||||||
let hasChanges = false;
|
let hasChanges = false;
|
||||||
|
|
||||||
for (const item of shoppingList.value.listItems) {
|
for (const item of shoppingList.value.listItems) {
|
||||||
const labelName = item.label?.name;
|
const labelName = item.label?.name || i18n.tc("shopping-list.no-label");
|
||||||
if (labelName && !existingLabels.has(labelName) && !(labelName in labelOpenState.value)) {
|
if (!existingLabels.has(labelName) && !(labelName in labelOpenState.value)) {
|
||||||
labelOpenState.value[labelName] = true;
|
labelOpenState.value[labelName] = true;
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
@@ -492,9 +492,13 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const labelNames = computed(() =>
|
const labelNames = computed(() => {
|
||||||
new Set(shoppingList.value?.listItems?.map(item => item.label?.name).filter(Boolean) ?? [])
|
return new Set(
|
||||||
|
shoppingList.value?.listItems
|
||||||
|
?.map(item => item.label?.name || i18n.tc("shopping-list.no-label"))
|
||||||
|
.filter(Boolean) ?? []
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
watch(labelNames, initializeLabelOpenStates, { immediate: true });
|
watch(labelNames, initializeLabelOpenStates, { immediate: true });
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface SideBarLink {
|
|||||||
href?: string;
|
href?: string;
|
||||||
title: string;
|
title: string;
|
||||||
children?: SideBarLink[];
|
children?: SideBarLink[];
|
||||||
|
childrenStartExpanded?: boolean;
|
||||||
restricted: boolean;
|
restricted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,20 +64,23 @@ async def lifespan_fn(_: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
settings.model_dump_json(
|
settings.model_dump_json(
|
||||||
indent=4,
|
indent=4,
|
||||||
exclude={
|
exclude={
|
||||||
"LDAP_QUERY_PASSWORD",
|
|
||||||
"OPENAI_API_KEY",
|
|
||||||
"SECRET",
|
"SECRET",
|
||||||
"SESSION_SECRET",
|
"SESSION_SECRET",
|
||||||
"SFTP_PASSWORD",
|
|
||||||
"SFTP_USERNAME",
|
|
||||||
"DB_URL", # replace by DB_URL_PUBLIC for logs
|
"DB_URL", # replace by DB_URL_PUBLIC for logs
|
||||||
"DB_PROVIDER",
|
"DB_PROVIDER",
|
||||||
"SMTP_USER",
|
|
||||||
"SMTP_PASSWORD",
|
|
||||||
"OIDC_CLIENT_SECRET",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
logger.info("------APP FEATURES------")
|
||||||
|
logger.info("--------==SMTP==--------")
|
||||||
|
logger.info(settings.SMTP_FEATURE)
|
||||||
|
logger.info("--------==LDAP==--------")
|
||||||
|
logger.info(settings.LDAP_FEATURE)
|
||||||
|
logger.info("--------==OIDC==--------")
|
||||||
|
logger.info(settings.OIDC_FEATURE)
|
||||||
|
logger.info("-------==OPENAI==-------")
|
||||||
|
logger.info(settings.OPENAI_FEATURE)
|
||||||
|
logger.info("------------------------")
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import os
|
|||||||
import secrets
|
import secrets
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, NamedTuple
|
from typing import Annotated, Any, NamedTuple
|
||||||
|
|
||||||
from dateutil.tz import tzlocal
|
from dateutil.tz import tzlocal
|
||||||
from pydantic import field_validator
|
from pydantic import PlainSerializer, field_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
from mealie.core.settings.themes import Theme
|
from mealie.core.settings.themes import Theme
|
||||||
@@ -19,6 +19,29 @@ class ScheduleTime(NamedTuple):
|
|||||||
minute: int
|
minute: int
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureDetails(NamedTuple):
|
||||||
|
enabled: bool
|
||||||
|
"""Indicates if the feature is enabled or not"""
|
||||||
|
description: str | None
|
||||||
|
"""Short description describing why the feature is not ready"""
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
s = f"Enabled: {self.enabled}"
|
||||||
|
if not self.enabled and self.description:
|
||||||
|
s += f"\nReason: {self.description}"
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
MaskedNoneString = Annotated[
|
||||||
|
str | None,
|
||||||
|
PlainSerializer(lambda x: None if x is None else "*****", return_type=str | None),
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
Custom serializer for sensitive settings. If the setting is None, then will serialize as null, otherwise,
|
||||||
|
the secret will be serialized as '*****'
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def determine_secrets(data_dir: Path, secret: str, production: bool) -> str:
|
def determine_secrets(data_dir: Path, secret: str, production: bool) -> str:
|
||||||
if not production:
|
if not production:
|
||||||
return "shh-secret-test-key"
|
return "shh-secret-test-key"
|
||||||
@@ -200,12 +223,16 @@ class AppSettings(AppLoggingSettings):
|
|||||||
SMTP_PORT: str | None = "587"
|
SMTP_PORT: str | None = "587"
|
||||||
SMTP_FROM_NAME: str | None = "Mealie"
|
SMTP_FROM_NAME: str | None = "Mealie"
|
||||||
SMTP_FROM_EMAIL: str | None = None
|
SMTP_FROM_EMAIL: str | None = None
|
||||||
SMTP_USER: str | None = None
|
SMTP_USER: MaskedNoneString = None
|
||||||
SMTP_PASSWORD: str | None = None
|
SMTP_PASSWORD: MaskedNoneString = None
|
||||||
SMTP_AUTH_STRATEGY: str | None = "TLS" # Options: 'TLS', 'SSL', 'NONE'
|
SMTP_AUTH_STRATEGY: str | None = "TLS" # Options: 'TLS', 'SSL', 'NONE'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def SMTP_ENABLE(self) -> bool:
|
def SMTP_ENABLE(self) -> bool:
|
||||||
|
return self.SMTP_FEATURE.enabled
|
||||||
|
|
||||||
|
@property
|
||||||
|
def SMTP_FEATURE(self) -> FeatureDetails:
|
||||||
return AppSettings.validate_smtp(
|
return AppSettings.validate_smtp(
|
||||||
self.SMTP_HOST,
|
self.SMTP_HOST,
|
||||||
self.SMTP_PORT,
|
self.SMTP_PORT,
|
||||||
@@ -225,15 +252,30 @@ class AppSettings(AppLoggingSettings):
|
|||||||
strategy: str | None = None,
|
strategy: str | None = None,
|
||||||
user: str | None = None,
|
user: str | None = None,
|
||||||
password: str | None = None,
|
password: str | None = None,
|
||||||
) -> bool:
|
) -> FeatureDetails:
|
||||||
"""Validates all SMTP variables are set"""
|
"""Validates all SMTP variables are set"""
|
||||||
required = {host, port, from_name, from_email, strategy}
|
description = None
|
||||||
|
required = {
|
||||||
|
"SMTP_HOST": host,
|
||||||
|
"SMTP_PORT": port,
|
||||||
|
"SMTP_FROM_NAME": from_name,
|
||||||
|
"SMTP_FROM_EMAIL": from_email,
|
||||||
|
"SMTP_AUTH_STRATEGY": strategy,
|
||||||
|
}
|
||||||
|
missing_values = [key for (key, value) in required.items() if value is None]
|
||||||
|
if missing_values:
|
||||||
|
description = f"Missing required values for {missing_values}"
|
||||||
|
|
||||||
if strategy and strategy.upper() in {"TLS", "SSL"}:
|
if strategy and strategy.upper() in {"TLS", "SSL"}:
|
||||||
required.add(user)
|
required["SMTP_USER"] = user
|
||||||
required.add(password)
|
required["SMTP_PASSWORD"] = password
|
||||||
|
if not description:
|
||||||
|
missing_values = [key for (key, value) in required.items() if value is None]
|
||||||
|
description = f"Missing required values for {missing_values} because SMTP_AUTH_STRATEGY is not None"
|
||||||
|
|
||||||
return "" not in required and None not in required
|
not_none = "" not in required.values() and None not in required.values()
|
||||||
|
|
||||||
|
return FeatureDetails(enabled=not_none, description=description)
|
||||||
|
|
||||||
# ===============================================
|
# ===============================================
|
||||||
# LDAP Configuration
|
# LDAP Configuration
|
||||||
@@ -245,31 +287,43 @@ class AppSettings(AppLoggingSettings):
|
|||||||
LDAP_ENABLE_STARTTLS: bool = False
|
LDAP_ENABLE_STARTTLS: bool = False
|
||||||
LDAP_BASE_DN: str | None = None
|
LDAP_BASE_DN: str | None = None
|
||||||
LDAP_QUERY_BIND: str | None = None
|
LDAP_QUERY_BIND: str | None = None
|
||||||
LDAP_QUERY_PASSWORD: str | None = None
|
LDAP_QUERY_PASSWORD: MaskedNoneString = None
|
||||||
LDAP_USER_FILTER: str | None = None
|
LDAP_USER_FILTER: str | None = None
|
||||||
LDAP_ADMIN_FILTER: str | None = None
|
LDAP_ADMIN_FILTER: str | None = None
|
||||||
LDAP_ID_ATTRIBUTE: str = "uid"
|
LDAP_ID_ATTRIBUTE: str = "uid"
|
||||||
LDAP_MAIL_ATTRIBUTE: str = "mail"
|
LDAP_MAIL_ATTRIBUTE: str = "mail"
|
||||||
LDAP_NAME_ATTRIBUTE: str = "name"
|
LDAP_NAME_ATTRIBUTE: str = "name"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def LDAP_FEATURE(self) -> FeatureDetails:
|
||||||
|
description = None if self.LDAP_AUTH_ENABLED else "LDAP_AUTH_ENABLED is false"
|
||||||
|
required = {
|
||||||
|
"LDAP_SERVER_URL": self.LDAP_SERVER_URL,
|
||||||
|
"LDAP_BASE_DN": self.LDAP_BASE_DN,
|
||||||
|
"LDAP_ID_ATTRIBUTE": self.LDAP_ID_ATTRIBUTE,
|
||||||
|
"LDAP_MAIL_ATTRIBUTE": self.LDAP_MAIL_ATTRIBUTE,
|
||||||
|
"LDAP_NAME_ATTRIBUTE": self.LDAP_NAME_ATTRIBUTE,
|
||||||
|
}
|
||||||
|
not_none = None not in required.values()
|
||||||
|
if not not_none and not description:
|
||||||
|
missing_values = [key for (key, value) in required.items() if value is None]
|
||||||
|
description = f"Missing required values for {missing_values}"
|
||||||
|
|
||||||
|
return FeatureDetails(
|
||||||
|
enabled=self.LDAP_AUTH_ENABLED and not_none,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def LDAP_ENABLED(self) -> bool:
|
def LDAP_ENABLED(self) -> bool:
|
||||||
"""Validates LDAP settings are all set"""
|
"""Validates LDAP settings are all set"""
|
||||||
required = {
|
return self.LDAP_FEATURE.enabled
|
||||||
self.LDAP_SERVER_URL,
|
|
||||||
self.LDAP_BASE_DN,
|
|
||||||
self.LDAP_ID_ATTRIBUTE,
|
|
||||||
self.LDAP_MAIL_ATTRIBUTE,
|
|
||||||
self.LDAP_NAME_ATTRIBUTE,
|
|
||||||
}
|
|
||||||
not_none = None not in required
|
|
||||||
return self.LDAP_AUTH_ENABLED and not_none
|
|
||||||
|
|
||||||
# ===============================================
|
# ===============================================
|
||||||
# OIDC Configuration
|
# OIDC Configuration
|
||||||
OIDC_AUTH_ENABLED: bool = False
|
OIDC_AUTH_ENABLED: bool = False
|
||||||
OIDC_CLIENT_ID: str | None = None
|
OIDC_CLIENT_ID: str | None = None
|
||||||
OIDC_CLIENT_SECRET: str | None = None
|
OIDC_CLIENT_SECRET: MaskedNoneString = None
|
||||||
OIDC_CONFIGURATION_URL: str | None = None
|
OIDC_CONFIGURATION_URL: str | None = None
|
||||||
OIDC_SIGNUP_ENABLED: bool = True
|
OIDC_SIGNUP_ENABLED: bool = True
|
||||||
OIDC_USER_GROUP: str | None = None
|
OIDC_USER_GROUP: str | None = None
|
||||||
@@ -279,6 +333,7 @@ class AppSettings(AppLoggingSettings):
|
|||||||
OIDC_REMEMBER_ME: bool = False
|
OIDC_REMEMBER_ME: bool = False
|
||||||
OIDC_USER_CLAIM: str = "email"
|
OIDC_USER_CLAIM: str = "email"
|
||||||
OIDC_GROUPS_CLAIM: str | None = "groups"
|
OIDC_GROUPS_CLAIM: str | None = "groups"
|
||||||
|
OIDC_SCOPES_OVERRIDE: str | None = None
|
||||||
OIDC_TLS_CACERTFILE: str | None = None
|
OIDC_TLS_CACERTFILE: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -286,29 +341,41 @@ class AppSettings(AppLoggingSettings):
|
|||||||
return self.OIDC_USER_GROUP is not None or self.OIDC_ADMIN_GROUP is not None
|
return self.OIDC_USER_GROUP is not None or self.OIDC_ADMIN_GROUP is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def OIDC_READY(self) -> bool:
|
def OIDC_FEATURE(self) -> FeatureDetails:
|
||||||
"""Validates OIDC settings are all set"""
|
description = None if self.OIDC_AUTH_ENABLED else "OIDC_AUTH_ENABLED is false"
|
||||||
|
|
||||||
required = {
|
required = {
|
||||||
self.OIDC_CLIENT_ID,
|
"OIDC_CLIENT_ID": self.OIDC_CLIENT_ID,
|
||||||
self.OIDC_CLIENT_SECRET,
|
"OIDC_CLIENT_SECRET": self.OIDC_CLIENT_SECRET,
|
||||||
self.OIDC_CONFIGURATION_URL,
|
"OIDC_CONFIGURATION_URL": self.OIDC_CONFIGURATION_URL,
|
||||||
self.OIDC_USER_CLAIM,
|
"OIDC_USER_CLAIM": self.OIDC_USER_CLAIM,
|
||||||
}
|
}
|
||||||
not_none = None not in required
|
not_none = None not in required.values()
|
||||||
valid_group_claim = True
|
if not not_none and not description:
|
||||||
|
missing_values = [key for (key, value) in required.items() if value is None]
|
||||||
|
description = f"Missing required values for {missing_values}"
|
||||||
|
|
||||||
|
valid_group_claim = True
|
||||||
if self.OIDC_REQUIRES_GROUP_CLAIM and self.OIDC_GROUPS_CLAIM is None:
|
if self.OIDC_REQUIRES_GROUP_CLAIM and self.OIDC_GROUPS_CLAIM is None:
|
||||||
|
if not description:
|
||||||
|
description = "OIDC_GROUPS_CLAIM is required when OIDC_USER_GROUP or OIDC_ADMIN_GROUP are provided"
|
||||||
valid_group_claim = False
|
valid_group_claim = False
|
||||||
|
|
||||||
return self.OIDC_AUTH_ENABLED and not_none and valid_group_claim
|
return FeatureDetails(
|
||||||
|
enabled=self.OIDC_AUTH_ENABLED and not_none and valid_group_claim,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def OIDC_READY(self) -> bool:
|
||||||
|
"""Validates OIDC settings are all set"""
|
||||||
|
return self.OIDC_FEATURE.enabled
|
||||||
|
|
||||||
# ===============================================
|
# ===============================================
|
||||||
# OpenAI Configuration
|
# OpenAI Configuration
|
||||||
|
|
||||||
OPENAI_BASE_URL: str | None = None
|
OPENAI_BASE_URL: str | None = None
|
||||||
"""The base URL for the OpenAI API. Leave this unset for most usecases"""
|
"""The base URL for the OpenAI API. Leave this unset for most usecases"""
|
||||||
OPENAI_API_KEY: str | None = None
|
OPENAI_API_KEY: MaskedNoneString = None
|
||||||
"""Your OpenAI API key. Required to enable OpenAI features"""
|
"""Your OpenAI API key. Required to enable OpenAI features"""
|
||||||
OPENAI_MODEL: str = "gpt-4o"
|
OPENAI_MODEL: str = "gpt-4o"
|
||||||
"""Which OpenAI model to send requests to. Leave this unset for most usecases"""
|
"""Which OpenAI model to send requests to. Leave this unset for most usecases"""
|
||||||
@@ -333,6 +400,24 @@ class AppSettings(AppLoggingSettings):
|
|||||||
The number of seconds to wait for an OpenAI request to complete before cancelling the request
|
The number of seconds to wait for an OpenAI request to complete before cancelling the request
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def OPENAI_FEATURE(self) -> FeatureDetails:
|
||||||
|
description = None
|
||||||
|
if not self.OPENAI_API_KEY:
|
||||||
|
description = "OPENAI_API_KEY is not set"
|
||||||
|
elif self.OPENAI_MODEL:
|
||||||
|
description = "OPENAI_MODEL is not set"
|
||||||
|
|
||||||
|
return FeatureDetails(
|
||||||
|
enabled=bool(self.OPENAI_API_KEY and self.OPENAI_MODEL),
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def OPENAI_ENABLED(self) -> bool:
|
||||||
|
"""Validates OpenAI settings are all set"""
|
||||||
|
return self.OPENAI_FEATURE.enabled
|
||||||
|
|
||||||
# ===============================================
|
# ===============================================
|
||||||
# Web Concurrency
|
# Web Concurrency
|
||||||
|
|
||||||
@@ -346,13 +431,17 @@ class AppSettings(AppLoggingSettings):
|
|||||||
def WORKERS(self) -> int:
|
def WORKERS(self) -> int:
|
||||||
return max(1, self.WORKER_PER_CORE * self.UVICORN_WORKERS)
|
return max(1, self.WORKER_PER_CORE * self.UVICORN_WORKERS)
|
||||||
|
|
||||||
@property
|
|
||||||
def OPENAI_ENABLED(self) -> bool:
|
|
||||||
"""Validates OpenAI settings are all set"""
|
|
||||||
return bool(self.OPENAI_API_KEY and self.OPENAI_MODEL)
|
|
||||||
|
|
||||||
model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow")
|
model_config = SettingsConfigDict(arbitrary_types_allowed=True, extra="allow")
|
||||||
|
|
||||||
|
# ===============================================
|
||||||
|
# TLS
|
||||||
|
|
||||||
|
TLS_CERTIFICATE_PATH: str | os.PathLike[str] | None = None
|
||||||
|
"""Path where the certificate resides."""
|
||||||
|
|
||||||
|
TLS_PRIVATE_KEY_PATH: str | os.PathLike[str] | None = None
|
||||||
|
"""Path where the private key resides."""
|
||||||
|
|
||||||
|
|
||||||
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:
|
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -151,6 +151,14 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||||||
else:
|
else:
|
||||||
self.household = None
|
self.household = None
|
||||||
|
|
||||||
|
if self.group is None:
|
||||||
|
raise ValueError(f"Group {group} does not exist; cannot create user")
|
||||||
|
if self.household is None:
|
||||||
|
raise ValueError(
|
||||||
|
f'Household "{household}" does not exist on group '
|
||||||
|
f'"{self.group.name}" ({self.group.id}); cannot create user'
|
||||||
|
)
|
||||||
|
|
||||||
self.rated_recipes = []
|
self.rated_recipes = []
|
||||||
|
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ def main():
|
|||||||
log_config=log_config(),
|
log_config=log_config(),
|
||||||
workers=settings.WORKERS,
|
workers=settings.WORKERS,
|
||||||
forwarded_allow_ips=settings.HOST_IP,
|
forwarded_allow_ips=settings.HOST_IP,
|
||||||
|
ssl_keyfile=settings.TLS_PRIVATE_KEY_PATH,
|
||||||
|
ssl_certfile=settings.TLS_CERTIFICATE_PATH,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from collections.abc import Callable
|
|||||||
from logging import Logger
|
from logging import Logger
|
||||||
from typing import Generic, TypeVar
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
|
import sqlalchemy.exc
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from pydantic import UUID4, BaseModel
|
from pydantic import UUID4, BaseModel
|
||||||
|
|
||||||
@@ -57,6 +58,12 @@ class HttpRepo(Generic[C, R, U]):
|
|||||||
# Respond
|
# Respond
|
||||||
msg = self.get_exception_message(ex)
|
msg = self.get_exception_message(ex)
|
||||||
|
|
||||||
|
if isinstance(ex, sqlalchemy.exc.NoResultFound):
|
||||||
|
raise HTTPException(
|
||||||
|
status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=ErrorResponse.respond(message=msg, exception=str(ex)),
|
||||||
|
)
|
||||||
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status.HTTP_400_BAD_REQUEST,
|
status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ErrorResponse.respond(message=msg, exception=str(ex)),
|
detail=ErrorResponse.respond(message=msg, exception=str(ex)),
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ remember_me_duration = timedelta(days=14)
|
|||||||
settings = get_app_settings()
|
settings = get_app_settings()
|
||||||
if settings.OIDC_READY:
|
if settings.OIDC_READY:
|
||||||
oauth = OAuth()
|
oauth = OAuth()
|
||||||
|
scope = None
|
||||||
|
if settings.OIDC_SCOPES_OVERRIDE:
|
||||||
|
scope = settings.OIDC_SCOPES_OVERRIDE
|
||||||
|
else:
|
||||||
groups_claim = settings.OIDC_GROUPS_CLAIM if settings.OIDC_REQUIRES_GROUP_CLAIM else ""
|
groups_claim = settings.OIDC_GROUPS_CLAIM if settings.OIDC_REQUIRES_GROUP_CLAIM else ""
|
||||||
scope = f"openid email profile {groups_claim}"
|
scope = f"openid email profile {groups_claim}"
|
||||||
client_args = {"scope": scope.rstrip()}
|
client_args = {"scope": scope.rstrip()}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.core.exceptions import mealie_registered_exceptions
|
from mealie.core.exceptions import mealie_registered_exceptions
|
||||||
|
from mealie.repos.all_repositories import get_repositories
|
||||||
from mealie.routes._base import BaseCrudController, controller
|
from mealie.routes._base import BaseCrudController, controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.routes._base.routers import MealieCrudRoute
|
from mealie.routes._base.routers import MealieCrudRoute
|
||||||
@@ -26,9 +27,13 @@ router = APIRouter(prefix="/households/cookbooks", tags=["Households: Cookbooks"
|
|||||||
@controller(router)
|
@controller(router)
|
||||||
class GroupCookbookController(BaseCrudController):
|
class GroupCookbookController(BaseCrudController):
|
||||||
@cached_property
|
@cached_property
|
||||||
def repo(self):
|
def cookbooks(self):
|
||||||
return self.repos.cookbooks
|
return self.repos.cookbooks
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def group_cookbooks(self):
|
||||||
|
return get_repositories(self.session, group_id=self.group_id, household_id=None).cookbooks
|
||||||
|
|
||||||
def registered_exceptions(self, ex: type[Exception]) -> str:
|
def registered_exceptions(self, ex: type[Exception]) -> str:
|
||||||
registered = {
|
registered = {
|
||||||
**mealie_registered_exceptions(self.translator),
|
**mealie_registered_exceptions(self.translator),
|
||||||
@@ -38,14 +43,15 @@ class GroupCookbookController(BaseCrudController):
|
|||||||
@cached_property
|
@cached_property
|
||||||
def mixins(self):
|
def mixins(self):
|
||||||
return HttpRepo[CreateCookBook, ReadCookBook, UpdateCookBook](
|
return HttpRepo[CreateCookBook, ReadCookBook, UpdateCookBook](
|
||||||
self.repo,
|
self.cookbooks,
|
||||||
self.logger,
|
self.logger,
|
||||||
self.registered_exceptions,
|
self.registered_exceptions,
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("", response_model=CookBookPagination)
|
@router.get("", response_model=CookBookPagination)
|
||||||
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
response = self.repo.page_all(
|
# Fetch all cookbooks for the group, rather than the household
|
||||||
|
response = self.group_cookbooks.page_all(
|
||||||
pagination=q,
|
pagination=q,
|
||||||
override=ReadCookBook,
|
override=ReadCookBook,
|
||||||
)
|
)
|
||||||
@@ -106,7 +112,8 @@ class GroupCookbookController(BaseCrudController):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
match_attr = "slug"
|
match_attr = "slug"
|
||||||
|
|
||||||
cookbook = self.repo.get_one(item_id, match_attr)
|
# Allow fetching other households' cookbooks
|
||||||
|
cookbook = self.group_cookbooks.get_one(item_id, match_attr)
|
||||||
|
|
||||||
if cookbook is None:
|
if cookbook is None:
|
||||||
raise HTTPException(status_code=404)
|
raise HTTPException(status_code=404)
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ class BaseRecipeController(BaseCrudController):
|
|||||||
return get_repositories(self.session, group_id=self.group_id, household_id=None).recipes
|
return get_repositories(self.session, group_id=self.group_id, household_id=None).recipes
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def cookbooks_repo(self) -> RepositoryGeneric[ReadCookBook, CookBook]:
|
def group_cookbooks(self) -> RepositoryGeneric[ReadCookBook, CookBook]:
|
||||||
return self.repos.cookbooks
|
return get_repositories(self.session, group_id=self.group_id, household_id=None).cookbooks
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def service(self) -> RecipeService:
|
def service(self) -> RecipeService:
|
||||||
@@ -354,7 +354,7 @@ class RecipeController(BaseRecipeController):
|
|||||||
cb_match_attr = "id"
|
cb_match_attr = "id"
|
||||||
except ValueError:
|
except ValueError:
|
||||||
cb_match_attr = "slug"
|
cb_match_attr = "slug"
|
||||||
cookbook_data = self.cookbooks_repo.get_one(search_query.cookbook, cb_match_attr)
|
cookbook_data = self.group_cookbooks.get_one(search_query.cookbook, cb_match_attr)
|
||||||
|
|
||||||
if cookbook_data is None:
|
if cookbook_data is None:
|
||||||
raise HTTPException(status_code=404, detail="cookbook not found")
|
raise HTTPException(status_code=404, detail="cookbook not found")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ def main():
|
|||||||
logger = root_logger.get_logger()
|
logger = root_logger.get_logger()
|
||||||
|
|
||||||
with session_context() as session:
|
with session_context() as session:
|
||||||
repos = AllRepositories(session)
|
repos = AllRepositories(session, group_id=None, household_id=None)
|
||||||
|
|
||||||
user = repos.users.get_one(confirmed, "email")
|
user = repos.users.get_one(confirmed, "email")
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ def main():
|
|||||||
logger = root_logger.get_logger()
|
logger = root_logger.get_logger()
|
||||||
|
|
||||||
with session_context() as session:
|
with session_context() as session:
|
||||||
repos = AllRepositories(session)
|
repos = AllRepositories(session, group_id=None, household_id=None)
|
||||||
user_service = UserService(repos)
|
user_service = UserService(repos)
|
||||||
|
|
||||||
locked_users = user_service.get_locked_users()
|
locked_users = user_service.get_locked_users()
|
||||||
|
|||||||
208
poetry.lock
generated
208
poetry.lock
generated
@@ -13,13 +13,13 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alembic"
|
name = "alembic"
|
||||||
version = "1.13.3"
|
version = "1.14.0"
|
||||||
description = "A database migration tool for SQLAlchemy."
|
description = "A database migration tool for SQLAlchemy."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "alembic-1.13.3-py3-none-any.whl", hash = "sha256:908e905976d15235fae59c9ac42c4c5b75cfcefe3d27c0fbf7ae15a37715d80e"},
|
{file = "alembic-1.14.0-py3-none-any.whl", hash = "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25"},
|
||||||
{file = "alembic-1.13.3.tar.gz", hash = "sha256:203503117415561e203aa14541740643a611f641517f0209fcae63e9fa09f1a2"},
|
{file = "alembic-1.14.0.tar.gz", hash = "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -603,13 +603,13 @@ test = ["pytest (>=6)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "extruct"
|
name = "extruct"
|
||||||
version = "0.17.0"
|
version = "0.18.0"
|
||||||
description = "Extract embedded metadata from HTML markup"
|
description = "Extract embedded metadata from HTML markup"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "extruct-0.17.0-py2.py3-none-any.whl", hash = "sha256:5f1d8e307fbb0c41f64ce486ddfaf16dc67e4b8f6e9570c57b123409ee37a307"},
|
{file = "extruct-0.18.0-py2.py3-none-any.whl", hash = "sha256:1e739985da705c3348c7614dc169e7780caf20908338fa5f4c6e48576df6f000"},
|
||||||
{file = "extruct-0.17.0.tar.gz", hash = "sha256:a94c0be5b5fd95a8370204ecc02687bd27845d536055d8d1c69a0a30da0420c7"},
|
{file = "extruct-0.18.0.tar.gz", hash = "sha256:b5b48d459003b27c05ee91527b14a5a31735231aaf85d2b1f331d4db879318dd"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -627,13 +627,13 @@ cli = ["requests"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.115.3"
|
version = "0.115.4"
|
||||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "fastapi-0.115.3-py3-none-any.whl", hash = "sha256:8035e8f9a2b0aa89cea03b6c77721178ed5358e1aea4cd8570d9466895c0638c"},
|
{file = "fastapi-0.115.4-py3-none-any.whl", hash = "sha256:0b504a063ffb3cf96a5e27dc1bc32c80ca743a2528574f9cdc77daa2d31b4742"},
|
||||||
{file = "fastapi-0.115.3.tar.gz", hash = "sha256:c091c6a35599c036d676fa24bd4a6e19fa30058d93d950216cdc672881f6f7db"},
|
{file = "fastapi-0.115.4.tar.gz", hash = "sha256:db653475586b091cb8b2fec2ac54a680ac6a158e07406e1abae31679e8826349"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1464,13 +1464,13 @@ pyyaml = ">=5.1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mkdocs-material"
|
name = "mkdocs-material"
|
||||||
version = "9.5.42"
|
version = "9.5.44"
|
||||||
description = "Documentation that simply works"
|
description = "Documentation that simply works"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "mkdocs_material-9.5.42-py3-none-any.whl", hash = "sha256:452a7c5d21284b373f36b981a2cbebfff59263feebeede1bc28652e9c5bbe316"},
|
{file = "mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca"},
|
||||||
{file = "mkdocs_material-9.5.42.tar.gz", hash = "sha256:92779b5e9b5934540c574c11647131d217dc540dce72b05feeda088c8eb1b8f2"},
|
{file = "mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1598,13 +1598,13 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "1.52.2"
|
version = "1.54.3"
|
||||||
description = "The official Python library for the openai API"
|
description = "The official Python library for the openai API"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7.1"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "openai-1.52.2-py3-none-any.whl", hash = "sha256:57e9e37bc407f39bb6ec3a27d7e8fb9728b2779936daa1fcf95df17d3edfaccc"},
|
{file = "openai-1.54.3-py3-none-any.whl", hash = "sha256:f18dbaf09c50d70c4185b892a2a553f80681d1d866323a2da7f7be2f688615d5"},
|
||||||
{file = "openai-1.52.2.tar.gz", hash = "sha256:87b7d0f69d85f5641678d414b7ee3082363647a5c66a462ed7f3ccb59582da0d"},
|
{file = "openai-1.54.3.tar.gz", hash = "sha256:7511b74eeb894ac0b0253dc71f087a15d2e4d71d22d0088767205143d880cca6"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1622,69 +1622,69 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "orjson"
|
name = "orjson"
|
||||||
version = "3.10.10"
|
version = "3.10.11"
|
||||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "orjson-3.10.10-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b788a579b113acf1c57e0a68e558be71d5d09aa67f62ca1f68e01117e550a998"},
|
{file = "orjson-3.10.11-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6dade64687f2bd7c090281652fe18f1151292d567a9302b34c2dbb92a3872f1f"},
|
||||||
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:804b18e2b88022c8905bb79bd2cbe59c0cd014b9328f43da8d3b28441995cda4"},
|
{file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82f07c550a6ccd2b9290849b22316a609023ed851a87ea888c0456485a7d196a"},
|
||||||
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9972572a1d042ec9ee421b6da69f7cc823da5962237563fa548ab17f152f0b9b"},
|
{file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd9a187742d3ead9df2e49240234d728c67c356516cf4db018833a86f20ec18c"},
|
||||||
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc6993ab1c2ae7dd0711161e303f1db69062955ac2668181bfdf2dd410e65258"},
|
{file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77b0fed6f209d76c1c39f032a70df2d7acf24b1812ca3e6078fd04e8972685a3"},
|
||||||
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d78e4cacced5781b01d9bc0f0cd8b70b906a0e109825cb41c1b03f9c41e4ce86"},
|
{file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63fc9d5fe1d4e8868f6aae547a7b8ba0a2e592929245fff61d633f4caccdcdd6"},
|
||||||
{file = "orjson-3.10.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6eb2598df518281ba0cbc30d24c5b06124ccf7e19169e883c14e0831217a0bc"},
|
{file = "orjson-3.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65cd3e3bb4fbb4eddc3c1e8dce10dc0b73e808fcb875f9fab40c81903dd9323e"},
|
||||||
{file = "orjson-3.10.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23776265c5215ec532de6238a52707048401a568f0fa0d938008e92a147fe2c7"},
|
{file = "orjson-3.10.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f67c570602300c4befbda12d153113b8974a3340fdcf3d6de095ede86c06d92"},
|
||||||
{file = "orjson-3.10.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cc2a654c08755cef90b468ff17c102e2def0edd62898b2486767204a7f5cc9c"},
|
{file = "orjson-3.10.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1f39728c7f7d766f1f5a769ce4d54b5aaa4c3f92d5b84817053cc9995b977acc"},
|
||||||
{file = "orjson-3.10.10-cp310-none-win32.whl", hash = "sha256:081b3fc6a86d72efeb67c13d0ea7c030017bd95f9868b1e329a376edc456153b"},
|
{file = "orjson-3.10.11-cp310-none-win32.whl", hash = "sha256:1789d9db7968d805f3d94aae2c25d04014aae3a2fa65b1443117cd462c6da647"},
|
||||||
{file = "orjson-3.10.10-cp310-none-win_amd64.whl", hash = "sha256:ff38c5fb749347768a603be1fb8a31856458af839f31f064c5aa74aca5be9efe"},
|
{file = "orjson-3.10.11-cp310-none-win_amd64.whl", hash = "sha256:5576b1e5a53a5ba8f8df81872bb0878a112b3ebb1d392155f00f54dd86c83ff6"},
|
||||||
{file = "orjson-3.10.10-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:879e99486c0fbb256266c7c6a67ff84f46035e4f8749ac6317cc83dacd7f993a"},
|
{file = "orjson-3.10.11-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1444f9cb7c14055d595de1036f74ecd6ce15f04a715e73f33bb6326c9cef01b6"},
|
||||||
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:019481fa9ea5ff13b5d5d95e6fd5ab25ded0810c80b150c2c7b1cc8660b662a7"},
|
{file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdec57fe3b4bdebcc08a946db3365630332dbe575125ff3d80a3272ebd0ddafe"},
|
||||||
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0dd57eff09894938b4c86d4b871a479260f9e156fa7f12f8cad4b39ea8028bb5"},
|
{file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eed32f33a0ea6ef36ccc1d37f8d17f28a1d6e8eefae5928f76aff8f1df85e67"},
|
||||||
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbde6d70cd95ab4d11ea8ac5e738e30764e510fc54d777336eec09bb93b8576c"},
|
{file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80df27dd8697242b904f4ea54820e2d98d3f51f91e97e358fc13359721233e4b"},
|
||||||
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2625cb37b8fb42e2147404e5ff7ef08712099197a9cd38895006d7053e69d6"},
|
{file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:705f03cee0cb797256d54de6695ef219e5bc8c8120b6654dd460848d57a9af3d"},
|
||||||
{file = "orjson-3.10.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbf3c20c6a7db69df58672a0d5815647ecf78c8e62a4d9bd284e8621c1fe5ccb"},
|
{file = "orjson-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03246774131701de8e7059b2e382597da43144a9a7400f178b2a32feafc54bd5"},
|
||||||
{file = "orjson-3.10.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:75c38f5647e02d423807d252ce4528bf6a95bd776af999cb1fb48867ed01d1f6"},
|
{file = "orjson-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8b5759063a6c940a69c728ea70d7c33583991c6982915a839c8da5f957e0103a"},
|
||||||
{file = "orjson-3.10.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23458d31fa50ec18e0ec4b0b4343730928296b11111df5f547c75913714116b2"},
|
{file = "orjson-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:677f23e32491520eebb19c99bb34675daf5410c449c13416f7f0d93e2cf5f981"},
|
||||||
{file = "orjson-3.10.10-cp311-none-win32.whl", hash = "sha256:2787cd9dedc591c989f3facd7e3e86508eafdc9536a26ec277699c0aa63c685b"},
|
{file = "orjson-3.10.11-cp311-none-win32.whl", hash = "sha256:a11225d7b30468dcb099498296ffac36b4673a8398ca30fdaec1e6c20df6aa55"},
|
||||||
{file = "orjson-3.10.10-cp311-none-win_amd64.whl", hash = "sha256:6514449d2c202a75183f807bc755167713297c69f1db57a89a1ef4a0170ee269"},
|
{file = "orjson-3.10.11-cp311-none-win_amd64.whl", hash = "sha256:df8c677df2f9f385fcc85ab859704045fa88d4668bc9991a527c86e710392bec"},
|
||||||
{file = "orjson-3.10.10-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8564f48f3620861f5ef1e080ce7cd122ee89d7d6dacf25fcae675ff63b4d6e05"},
|
{file = "orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51"},
|
||||||
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5bf161a32b479034098c5b81f2608f09167ad2fa1c06abd4e527ea6bf4837a9"},
|
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97"},
|
||||||
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b65c93617bcafa7f04b74ae8bc2cc214bd5cb45168a953256ff83015c6747d"},
|
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19"},
|
||||||
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8e28406f97fc2ea0c6150f4c1b6e8261453318930b334abc419214c82314f85"},
|
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0"},
|
||||||
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4d0d9fe174cc7a5bdce2e6c378bcdb4c49b2bf522a8f996aa586020e1b96cee"},
|
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433"},
|
||||||
{file = "orjson-3.10.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3be81c42f1242cbed03cbb3973501fcaa2675a0af638f8be494eaf37143d999"},
|
{file = "orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5"},
|
||||||
{file = "orjson-3.10.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65f9886d3bae65be026219c0a5f32dbbe91a9e6272f56d092ab22561ad0ea33b"},
|
{file = "orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd"},
|
||||||
{file = "orjson-3.10.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:730ed5350147db7beb23ddaf072f490329e90a1d059711d364b49fe352ec987b"},
|
{file = "orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b"},
|
||||||
{file = "orjson-3.10.10-cp312-none-win32.whl", hash = "sha256:a8f4bf5f1c85bea2170800020d53a8877812892697f9c2de73d576c9307a8a5f"},
|
{file = "orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d"},
|
||||||
{file = "orjson-3.10.10-cp312-none-win_amd64.whl", hash = "sha256:384cd13579a1b4cd689d218e329f459eb9ddc504fa48c5a83ef4889db7fd7a4f"},
|
{file = "orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284"},
|
||||||
{file = "orjson-3.10.10-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44bffae68c291f94ff5a9b4149fe9d1bdd4cd0ff0fb575bcea8351d48db629a1"},
|
{file = "orjson-3.10.11-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899"},
|
||||||
{file = "orjson-3.10.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e27b4c6437315df3024f0835887127dac2a0a3ff643500ec27088d2588fa5ae1"},
|
{file = "orjson-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230"},
|
||||||
{file = "orjson-3.10.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca84df16d6b49325a4084fd8b2fe2229cb415e15c46c529f868c3387bb1339d"},
|
{file = "orjson-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0"},
|
||||||
{file = "orjson-3.10.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c14ce70e8f39bd71f9f80423801b5d10bf93d1dceffdecd04df0f64d2c69bc01"},
|
{file = "orjson-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258"},
|
||||||
{file = "orjson-3.10.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:24ac62336da9bda1bd93c0491eff0613003b48d3cb5d01470842e7b52a40d5b4"},
|
{file = "orjson-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0"},
|
||||||
{file = "orjson-3.10.10-cp313-none-win32.whl", hash = "sha256:eb0a42831372ec2b05acc9ee45af77bcaccbd91257345f93780a8e654efc75db"},
|
{file = "orjson-3.10.11-cp313-none-win32.whl", hash = "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b"},
|
||||||
{file = "orjson-3.10.10-cp313-none-win_amd64.whl", hash = "sha256:f0c4f37f8bf3f1075c6cc8dd8a9f843689a4b618628f8812d0a71e6968b95ffd"},
|
{file = "orjson-3.10.11-cp313-none-win_amd64.whl", hash = "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270"},
|
||||||
{file = "orjson-3.10.10-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:829700cc18503efc0cf502d630f612884258020d98a317679cd2054af0259568"},
|
{file = "orjson-3.10.11-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:19b3763e8bbf8ad797df6b6b5e0fc7c843ec2e2fc0621398534e0c6400098f87"},
|
||||||
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0ceb5e0e8c4f010ac787d29ae6299846935044686509e2f0f06ed441c1ca949"},
|
{file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be83a13312e5e58d633580c5eb8d0495ae61f180da2722f20562974188af205"},
|
||||||
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c25908eb86968613216f3db4d3003f1c45d78eb9046b71056ca327ff92bdbd4"},
|
{file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:afacfd1ab81f46dedd7f6001b6d4e8de23396e4884cd3c3436bd05defb1a6446"},
|
||||||
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:218cb0bc03340144b6328a9ff78f0932e642199ac184dd74b01ad691f42f93ff"},
|
{file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb4d0bea56bba596723d73f074c420aec3b2e5d7d30698bc56e6048066bd560c"},
|
||||||
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2277ec2cea3775640dc81ab5195bb5b2ada2fe0ea6eee4677474edc75ea6785"},
|
{file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96ed1de70fcb15d5fed529a656df29f768187628727ee2788344e8a51e1c1350"},
|
||||||
{file = "orjson-3.10.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:848ea3b55ab5ccc9d7bbd420d69432628b691fba3ca8ae3148c35156cbd282aa"},
|
{file = "orjson-3.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bfb30c891b530f3f80e801e3ad82ef150b964e5c38e1fb8482441c69c35c61c"},
|
||||||
{file = "orjson-3.10.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e3e67b537ac0c835b25b5f7d40d83816abd2d3f4c0b0866ee981a045287a54f3"},
|
{file = "orjson-3.10.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d496c74fc2b61341e3cefda7eec21b7854c5f672ee350bc55d9a4997a8a95204"},
|
||||||
{file = "orjson-3.10.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:7948cfb909353fce2135dcdbe4521a5e7e1159484e0bb024c1722f272488f2b8"},
|
{file = "orjson-3.10.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:655a493bac606655db9a47fe94d3d84fc7f3ad766d894197c94ccf0c5408e7d3"},
|
||||||
{file = "orjson-3.10.10-cp38-none-win32.whl", hash = "sha256:78bee66a988f1a333dc0b6257503d63553b1957889c17b2c4ed72385cd1b96ae"},
|
{file = "orjson-3.10.11-cp38-none-win32.whl", hash = "sha256:b9546b278c9fb5d45380f4809e11b4dd9844ca7aaf1134024503e134ed226161"},
|
||||||
{file = "orjson-3.10.10-cp38-none-win_amd64.whl", hash = "sha256:f1d647ca8d62afeb774340a343c7fc023efacfd3a39f70c798991063f0c681dd"},
|
{file = "orjson-3.10.11-cp38-none-win_amd64.whl", hash = "sha256:b592597fe551d518f42c5a2eb07422eb475aa8cfdc8c51e6da7054b836b26782"},
|
||||||
{file = "orjson-3.10.10-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5a059afddbaa6dd733b5a2d76a90dbc8af790b993b1b5cb97a1176ca713b5df8"},
|
{file = "orjson-3.10.11-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c95f2ecafe709b4e5c733b5e2768ac569bed308623c85806c395d9cca00e08af"},
|
||||||
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f9b5c59f7e2a1a410f971c5ebc68f1995822837cd10905ee255f96074537ee6"},
|
{file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80c00d4acded0c51c98754fe8218cb49cb854f0f7eb39ea4641b7f71732d2cb7"},
|
||||||
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d5ef198bafdef4aa9d49a4165ba53ffdc0a9e1c7b6f76178572ab33118afea25"},
|
{file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:461311b693d3d0a060439aa669c74f3603264d4e7a08faa68c47ae5a863f352d"},
|
||||||
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf29ce0bb5d3320824ec3d1508652421000ba466abd63bdd52c64bcce9eb1fa"},
|
{file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52ca832f17d86a78cbab86cdc25f8c13756ebe182b6fc1a97d534051c18a08de"},
|
||||||
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dddd5516bcc93e723d029c1633ae79c4417477b4f57dad9bfeeb6bc0315e654a"},
|
{file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c57ea78a753812f528178aa2f1c57da633754c91d2124cb28991dab4c79a54"},
|
||||||
{file = "orjson-3.10.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12f2003695b10817f0fa8b8fca982ed7f5761dcb0d93cff4f2f9f6709903fd7"},
|
{file = "orjson-3.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7fcfc6f7ca046383fb954ba528587e0f9336828b568282b27579c49f8e16aad"},
|
||||||
{file = "orjson-3.10.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:672f9874a8a8fb9bb1b771331d31ba27f57702c8106cdbadad8bda5d10bc1019"},
|
{file = "orjson-3.10.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:86b9dd983857970c29e4c71bb3e95ff085c07d3e83e7c46ebe959bac07ebd80b"},
|
||||||
{file = "orjson-3.10.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1dcbb0ca5fafb2b378b2c74419480ab2486326974826bbf6588f4dc62137570a"},
|
{file = "orjson-3.10.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4d83f87582d223e54efb2242a79547611ba4ebae3af8bae1e80fa9a0af83bb7f"},
|
||||||
{file = "orjson-3.10.10-cp39-none-win32.whl", hash = "sha256:d9bbd3a4b92256875cb058c3381b782649b9a3c68a4aa9a2fff020c2f9cfc1be"},
|
{file = "orjson-3.10.11-cp39-none-win32.whl", hash = "sha256:9fd0ad1c129bc9beb1154c2655f177620b5beaf9a11e0d10bac63ef3fce96950"},
|
||||||
{file = "orjson-3.10.10-cp39-none-win_amd64.whl", hash = "sha256:766f21487a53aee8524b97ca9582d5c6541b03ab6210fbaf10142ae2f3ced2aa"},
|
{file = "orjson-3.10.11-cp39-none-win_amd64.whl", hash = "sha256:10f416b2a017c8bd17f325fb9dee1fb5cdd7a54e814284896b7c3f2763faa017"},
|
||||||
{file = "orjson-3.10.10.tar.gz", hash = "sha256:37949383c4df7b4337ce82ee35b6d7471e55195efa7dcb45ab8226ceadb0fe3b"},
|
{file = "orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2174,13 +2174,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-settings"
|
name = "pydantic-settings"
|
||||||
version = "2.6.0"
|
version = "2.6.1"
|
||||||
description = "Settings management using Pydantic"
|
description = "Settings management using Pydantic"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic_settings-2.6.0-py3-none-any.whl", hash = "sha256:4a819166f119b74d7f8c765196b165f95cc7487ce58ea27dec8a5a26be0970e0"},
|
{file = "pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87"},
|
||||||
{file = "pydantic_settings-2.6.0.tar.gz", hash = "sha256:44a1804abffac9e6a30372bb45f6cafab945ef5af25e66b1c634c01dd39e0188"},
|
{file = "pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2413,13 +2413,13 @@ pyasn1_modules = ">=0.1.5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-multipart"
|
name = "python-multipart"
|
||||||
version = "0.0.12"
|
version = "0.0.17"
|
||||||
description = "A streaming multipart parser for Python"
|
description = "A streaming multipart parser for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "python_multipart-0.0.12-py3-none-any.whl", hash = "sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf"},
|
{file = "python_multipart-0.0.17-py3-none-any.whl", hash = "sha256:15dc4f487e0a9476cc1201261188ee0940165cffc94429b6fc565c4d3045cb5d"},
|
||||||
{file = "python_multipart-0.0.12.tar.gz", hash = "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb"},
|
{file = "python_multipart-0.0.17.tar.gz", hash = "sha256:41330d831cae6e2f22902704ead2826ea038d0419530eadff3ea80175aec5538"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2796,13 +2796,13 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "13.9.3"
|
version = "13.9.4"
|
||||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.0"
|
python-versions = ">=3.8.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283"},
|
{file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
|
||||||
{file = "rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e"},
|
{file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2815,29 +2815,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.7.1"
|
version = "0.7.3"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.7.1-py3-none-linux_armv6l.whl", hash = "sha256:cb1bc5ed9403daa7da05475d615739cc0212e861b7306f314379d958592aaa89"},
|
{file = "ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344"},
|
||||||
{file = "ruff-0.7.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27c1c52a8d199a257ff1e5582d078eab7145129aa02721815ca8fa4f9612dc35"},
|
{file = "ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0"},
|
||||||
{file = "ruff-0.7.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:588a34e1ef2ea55b4ddfec26bbe76bc866e92523d8c6cdec5e8aceefeff02d99"},
|
{file = "ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9"},
|
||||||
{file = "ruff-0.7.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94fc32f9cdf72dc75c451e5f072758b118ab8100727168a3df58502b43a599ca"},
|
{file = "ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5"},
|
||||||
{file = "ruff-0.7.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:985818742b833bffa543a84d1cc11b5e6871de1b4e0ac3060a59a2bae3969250"},
|
{file = "ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299"},
|
||||||
{file = "ruff-0.7.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32f1e8a192e261366c702c5fb2ece9f68d26625f198a25c408861c16dc2dea9c"},
|
{file = "ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e"},
|
||||||
{file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:699085bf05819588551b11751eff33e9ca58b1b86a6843e1b082a7de40da1565"},
|
{file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29"},
|
||||||
{file = "ruff-0.7.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:344cc2b0814047dc8c3a8ff2cd1f3d808bb23c6658db830d25147339d9bf9ea7"},
|
{file = "ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5"},
|
||||||
{file = "ruff-0.7.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4316bbf69d5a859cc937890c7ac7a6551252b6a01b1d2c97e8fc96e45a7c8b4a"},
|
{file = "ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67"},
|
||||||
{file = "ruff-0.7.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79d3af9dca4c56043e738a4d6dd1e9444b6d6c10598ac52d146e331eb155a8ad"},
|
{file = "ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2"},
|
||||||
{file = "ruff-0.7.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5c121b46abde94a505175524e51891f829414e093cd8326d6e741ecfc0a9112"},
|
{file = "ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d"},
|
||||||
{file = "ruff-0.7.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8422104078324ea250886954e48f1373a8fe7de59283d747c3a7eca050b4e378"},
|
{file = "ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2"},
|
||||||
{file = "ruff-0.7.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:56aad830af8a9db644e80098fe4984a948e2b6fc2e73891538f43bbe478461b8"},
|
{file = "ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2"},
|
||||||
{file = "ruff-0.7.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:658304f02f68d3a83c998ad8bf91f9b4f53e93e5412b8f2388359d55869727fd"},
|
{file = "ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16"},
|
||||||
{file = "ruff-0.7.1-py3-none-win32.whl", hash = "sha256:b517a2011333eb7ce2d402652ecaa0ac1a30c114fbbd55c6b8ee466a7f600ee9"},
|
{file = "ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc"},
|
||||||
{file = "ruff-0.7.1-py3-none-win_amd64.whl", hash = "sha256:f38c41fcde1728736b4eb2b18850f6d1e3eedd9678c914dede554a70d5241307"},
|
{file = "ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088"},
|
||||||
{file = "ruff-0.7.1-py3-none-win_arm64.whl", hash = "sha256:19aa200ec824c0f36d0c9114c8ec0087082021732979a359d6f3c390a6ff2a37"},
|
{file = "ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c"},
|
||||||
{file = "ruff-0.7.1.tar.gz", hash = "sha256:9d8a41d4aa2dad1575adb98a82870cf5db5f76b2938cf2206c22c940034a36f4"},
|
{file = "ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3416,4 +3416,4 @@ pgsql = ["psycopg2-binary"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "455f4f29104e6614f7ff7e899cf2d63302cd84dd693b59ee17d48ca05fd39543"
|
content-hash = "2a3b97688c700f6c01241c0559afa48bdf039399261e7cdd68eebad96dadb44f"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ authors = ["Hayden <hay-kot@pm.me>"]
|
|||||||
description = "A Recipe Manager"
|
description = "A Recipe Manager"
|
||||||
license = "AGPL"
|
license = "AGPL"
|
||||||
name = "mealie"
|
name = "mealie"
|
||||||
version = "2.0.0"
|
version = "2.1.0"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
start = "mealie.app:main"
|
start = "mealie.app:main"
|
||||||
@@ -19,7 +19,7 @@ aniso8601 = "9.0.1"
|
|||||||
appdirs = "1.4.4"
|
appdirs = "1.4.4"
|
||||||
apprise = "^1.4.5"
|
apprise = "^1.4.5"
|
||||||
bcrypt = "^4.0.1"
|
bcrypt = "^4.0.1"
|
||||||
extruct = "^0.17.0"
|
extruct = "^0.18.0"
|
||||||
fastapi = "^0.115.0"
|
fastapi = "^0.115.0"
|
||||||
httpx = "^0.27.0"
|
httpx = "^0.27.0"
|
||||||
lxml = "^5.0.0"
|
lxml = "^5.0.0"
|
||||||
@@ -31,7 +31,7 @@ python = "^3.10"
|
|||||||
python-dateutil = "^2.8.2"
|
python-dateutil = "^2.8.2"
|
||||||
python-dotenv = "^1.0.0"
|
python-dotenv = "^1.0.0"
|
||||||
python-ldap = "^3.3.1"
|
python-ldap = "^3.3.1"
|
||||||
python-multipart = "^0.0.12"
|
python-multipart = "^0.0.17"
|
||||||
python-slugify = "^8.0.0"
|
python-slugify = "^8.0.0"
|
||||||
recipe-scrapers = "^15.0.0"
|
recipe-scrapers = "^15.0.0"
|
||||||
requests = "^2.31.0"
|
requests = "^2.31.0"
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ def test_create_cookbook(api_client: TestClient, unique_user: TestUser):
|
|||||||
page_data = get_page_data(unique_user.group_id, unique_user.household_id)
|
page_data = get_page_data(unique_user.group_id, unique_user.household_id)
|
||||||
response = api_client.post(api_routes.households_cookbooks, json=page_data, headers=unique_user.token)
|
response = api_client.post(api_routes.households_cookbooks, json=page_data, headers=unique_user.token)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
assert response.json()["groupId"] == unique_user.group_id
|
||||||
|
assert response.json()["householdId"] == unique_user.household_id
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("name_input", ["", " ", "@"])
|
@pytest.mark.parametrize("name_input", ["", " ", "@"])
|
||||||
@@ -78,9 +80,22 @@ def test_create_cookbook_bad_name(api_client: TestClient, unique_user: TestUser,
|
|||||||
assert response.status_code == 422
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
def test_read_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
|
@pytest.mark.parametrize("use_other_household", [True, False])
|
||||||
|
def test_read_cookbook(
|
||||||
|
api_client: TestClient,
|
||||||
|
unique_user: TestUser,
|
||||||
|
h2_user: TestUser,
|
||||||
|
cookbooks: list[TestCookbook],
|
||||||
|
use_other_household: bool,
|
||||||
|
):
|
||||||
sample = random.choice(cookbooks)
|
sample = random.choice(cookbooks)
|
||||||
response = api_client.get(api_routes.households_cookbooks_item_id(sample.id), headers=unique_user.token)
|
if use_other_household:
|
||||||
|
headers = h2_user.token
|
||||||
|
else:
|
||||||
|
headers = unique_user.token
|
||||||
|
|
||||||
|
# all households should be able to fetch all cookbooks
|
||||||
|
response = api_client.get(api_routes.households_cookbooks_item_id(sample.id), headers=headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
page_data = response.json()
|
page_data = response.json()
|
||||||
@@ -111,6 +126,28 @@ def test_update_cookbook(api_client: TestClient, unique_user: TestUser, cookbook
|
|||||||
assert page_data["slug"] == update_data["name"]
|
assert page_data["slug"] == update_data["name"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_cookbook_other_household(
|
||||||
|
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, cookbooks: list[TestCookbook]
|
||||||
|
):
|
||||||
|
cookbook = random.choice(cookbooks)
|
||||||
|
|
||||||
|
update_data = get_page_data(unique_user.group_id, unique_user.household_id)
|
||||||
|
|
||||||
|
update_data["name"] = random_string(10)
|
||||||
|
|
||||||
|
response = api_client.put(
|
||||||
|
api_routes.households_cookbooks_item_id(cookbook.id), json=update_data, headers=h2_user.token
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
response = api_client.get(api_routes.households_cookbooks_item_id(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]):
|
def test_update_cookbooks_many(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
|
||||||
pages = [x.data for x in cookbooks]
|
pages = [x.data for x in cookbooks]
|
||||||
|
|
||||||
@@ -135,6 +172,20 @@ def test_update_cookbooks_many(api_client: TestClient, unique_user: TestUser, co
|
|||||||
assert str(know) in server_ids
|
assert str(know) in server_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_cookbooks_many_other_household(
|
||||||
|
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, cookbooks: list[TestCookbook]
|
||||||
|
):
|
||||||
|
pages = [x.data for x in cookbooks]
|
||||||
|
|
||||||
|
reverse_order = sorted(pages, key=lambda x: x["position"], reverse=True)
|
||||||
|
for x, page in enumerate(reverse_order):
|
||||||
|
page["position"] = x
|
||||||
|
page["group_id"] = str(unique_user.group_id)
|
||||||
|
|
||||||
|
response = api_client.put(api_routes.households_cookbooks, json=utils.jsonify(reverse_order), headers=h2_user.token)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
def test_delete_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
|
def test_delete_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
|
||||||
sample = random.choice(cookbooks)
|
sample = random.choice(cookbooks)
|
||||||
response = api_client.delete(api_routes.households_cookbooks_item_id(sample.id), headers=unique_user.token)
|
response = api_client.delete(api_routes.households_cookbooks_item_id(sample.id), headers=unique_user.token)
|
||||||
@@ -145,6 +196,18 @@ def test_delete_cookbook(api_client: TestClient, unique_user: TestUser, cookbook
|
|||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_cookbook_other_household(
|
||||||
|
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, cookbooks: list[TestCookbook]
|
||||||
|
):
|
||||||
|
sample = random.choice(cookbooks)
|
||||||
|
response = api_client.delete(api_routes.households_cookbooks_item_id(sample.id), headers=h2_user.token)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
response = api_client.get(api_routes.households_cookbooks_item_id(sample.slug), headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"qf_string, expected_code",
|
"qf_string, expected_code",
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -299,3 +299,16 @@ def test_cookbook_recipes_includes_all_households(api_client: TestClient, unique
|
|||||||
assert recipe.id in fetched_recipe_ids
|
assert recipe.id in fetched_recipe_ids
|
||||||
for recipe in other_recipes:
|
for recipe in other_recipes:
|
||||||
assert recipe.id in fetched_recipe_ids
|
assert recipe.id in fetched_recipe_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_cookbooks_from_other_households(api_client: TestClient, unique_user: TestUser, h2_user: TestUser):
|
||||||
|
h2_cookbook = h2_user.repos.cookbooks.create(
|
||||||
|
SaveCookBook(
|
||||||
|
name=random_string(),
|
||||||
|
group_id=h2_user.group_id,
|
||||||
|
household_id=h2_user.household_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
response = api_client.get(api_routes.recipes, params={"cookbook": h2_cookbook.slug}, headers=unique_user.token)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import pytest
|
||||||
from pytest import MonkeyPatch, Session
|
from pytest import MonkeyPatch, Session
|
||||||
|
|
||||||
from mealie.core.config import get_app_settings
|
from mealie.core.config import get_app_settings
|
||||||
from mealie.core.security.providers.openid_provider import OpenIDProvider
|
from mealie.core.security.providers.openid_provider import OpenIDProvider
|
||||||
from mealie.repos.all_repositories import get_repositories
|
from mealie.repos.all_repositories import get_repositories
|
||||||
|
from tests.utils.factories import random_email, random_string
|
||||||
from tests.utils.fixture_schemas import TestUser
|
from tests.utils.fixture_schemas import TestUser
|
||||||
|
|
||||||
|
|
||||||
@@ -125,3 +127,38 @@ def test_has_admin_group_new_user(monkeypatch: MonkeyPatch, session: Session):
|
|||||||
user = db.users.get_one("dude2", "username")
|
user = db.users.get_one("dude2", "username")
|
||||||
assert user is not None
|
assert user is not None
|
||||||
assert user.admin
|
assert user.admin
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("valid_group", [True, False])
|
||||||
|
@pytest.mark.parametrize("valid_household", [True, False])
|
||||||
|
def test_ldap_user_creation_invalid_group_or_household(
|
||||||
|
monkeypatch: MonkeyPatch, session: Session, valid_group: bool, valid_household: bool
|
||||||
|
):
|
||||||
|
monkeypatch.setenv("OIDC_USER_GROUP", "mealie_user")
|
||||||
|
monkeypatch.setenv("OIDC_ADMIN_GROUP", "mealie_admin")
|
||||||
|
if not valid_group:
|
||||||
|
monkeypatch.setenv("DEFAULT_GROUP", random_string())
|
||||||
|
if not valid_household:
|
||||||
|
monkeypatch.setenv("DEFAULT_HOUSEHOLD", random_string())
|
||||||
|
get_app_settings.cache_clear()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"preferred_username": random_string(),
|
||||||
|
"email": random_email(),
|
||||||
|
"name": random_string(),
|
||||||
|
"groups": ["mealie_user"],
|
||||||
|
}
|
||||||
|
auth_provider = OpenIDProvider(session, data)
|
||||||
|
|
||||||
|
if valid_group and valid_household:
|
||||||
|
assert auth_provider.authenticate() is not None
|
||||||
|
else:
|
||||||
|
assert auth_provider.authenticate() is None
|
||||||
|
|
||||||
|
db = get_repositories(session, group_id=None, household_id=None)
|
||||||
|
user = db.users.get_one(data["preferred_username"], "username")
|
||||||
|
|
||||||
|
if valid_group and valid_household:
|
||||||
|
assert user is not None
|
||||||
|
else:
|
||||||
|
assert user is None
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
@@ -126,13 +127,27 @@ smtp_validation_cases = [
|
|||||||
(
|
(
|
||||||
"good_data_tls",
|
"good_data_tls",
|
||||||
SMTPValidationCase(
|
SMTPValidationCase(
|
||||||
"email.mealie.io", "587", "tls", "Mealie", "mealie@mealie.io", "mealie@mealie.io", "mealie-password", True
|
"email.mealie.io",
|
||||||
|
"587",
|
||||||
|
"tls",
|
||||||
|
"Mealie",
|
||||||
|
"mealie@mealie.io",
|
||||||
|
"mealie@mealie.io",
|
||||||
|
"mealie-password",
|
||||||
|
True,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"good_data_ssl",
|
"good_data_ssl",
|
||||||
SMTPValidationCase(
|
SMTPValidationCase(
|
||||||
"email.mealie.io", "465", "tls", "Mealie", "mealie@mealie.io", "mealie@mealie.io", "mealie-password", True
|
"email.mealie.io",
|
||||||
|
"465",
|
||||||
|
"tls",
|
||||||
|
"Mealie",
|
||||||
|
"mealie@mealie.io",
|
||||||
|
"mealie@mealie.io",
|
||||||
|
"mealie-password",
|
||||||
|
True,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -151,6 +166,149 @@ def test_smtp_enable_with_bad_data_tls(data: SMTPValidationCase):
|
|||||||
data.auth_strategy,
|
data.auth_strategy,
|
||||||
data.user,
|
data.user,
|
||||||
data.password,
|
data.password,
|
||||||
)
|
).enabled
|
||||||
|
|
||||||
assert is_valid is data.is_valid
|
assert is_valid is data.is_valid
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class EnvVar:
|
||||||
|
name: str
|
||||||
|
value: any
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPValidationCase:
|
||||||
|
settings = list[EnvVar]
|
||||||
|
is_valid: bool
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
enabled: bool,
|
||||||
|
server_url: str | None,
|
||||||
|
base_dn: str | None,
|
||||||
|
is_valid: bool,
|
||||||
|
):
|
||||||
|
self.settings = [
|
||||||
|
EnvVar("LDAP_AUTH_ENABLED", enabled),
|
||||||
|
EnvVar("LDAP_SERVER_URL", server_url),
|
||||||
|
EnvVar("LDAP_BASE_DN", base_dn),
|
||||||
|
]
|
||||||
|
self.is_valid = is_valid
|
||||||
|
|
||||||
|
|
||||||
|
ldap_validation_cases = [
|
||||||
|
("not enabled", LDAPValidationCase(False, None, None, False)),
|
||||||
|
("missing url", LDAPValidationCase(True, None, "dn", False)),
|
||||||
|
("missing base dn", LDAPValidationCase(True, "url", None, False)),
|
||||||
|
("all good", LDAPValidationCase(True, "url", "dn", True)),
|
||||||
|
]
|
||||||
|
|
||||||
|
ldap_cases = [x[1] for x in ldap_validation_cases]
|
||||||
|
ldap_cases_ids = [x[0] for x in ldap_validation_cases]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", ldap_cases, ids=ldap_cases_ids)
|
||||||
|
def test_ldap_settings_validation(data: LDAPValidationCase, monkeypatch: pytest.MonkeyPatch):
|
||||||
|
for setting in data.settings:
|
||||||
|
if setting.value is not None:
|
||||||
|
monkeypatch.setenv(setting.name, setting.value)
|
||||||
|
else:
|
||||||
|
monkeypatch.delenv(setting.name, raising=False)
|
||||||
|
|
||||||
|
get_app_settings.cache_clear()
|
||||||
|
app_settings = get_app_settings()
|
||||||
|
|
||||||
|
assert app_settings.LDAP_ENABLED is data.is_valid
|
||||||
|
|
||||||
|
|
||||||
|
class OIDCValidationCase:
|
||||||
|
settings = list[EnvVar]
|
||||||
|
is_valid: bool
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
enabled: bool,
|
||||||
|
client_id: str | None,
|
||||||
|
client_secret: str | None,
|
||||||
|
configuration_url: str | None,
|
||||||
|
groups_claim: str | None,
|
||||||
|
user_group: str | None,
|
||||||
|
admin_group: str | None,
|
||||||
|
is_valid: bool,
|
||||||
|
):
|
||||||
|
self.settings = [
|
||||||
|
EnvVar("OIDC_AUTH_ENABLED", enabled),
|
||||||
|
EnvVar("OIDC_CLIENT_ID", client_id),
|
||||||
|
EnvVar("OIDC_CLIENT_SECRET", client_secret),
|
||||||
|
EnvVar("OIDC_CONFIGURATION_URL", configuration_url),
|
||||||
|
EnvVar("OIDC_GROUPS_CLAIM", groups_claim),
|
||||||
|
EnvVar("OIDC_USER_GROUP", user_group),
|
||||||
|
EnvVar("OIDC_ADMIN_GROUP", admin_group),
|
||||||
|
]
|
||||||
|
self.is_valid = is_valid
|
||||||
|
|
||||||
|
|
||||||
|
oidc_validation_cases = [
|
||||||
|
(
|
||||||
|
"not enabled",
|
||||||
|
OIDCValidationCase(False, None, None, None, None, None, None, False),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"missing client id",
|
||||||
|
OIDCValidationCase(True, None, "secret", "url", "groups", "user", "admin", False),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"missing client secret",
|
||||||
|
OIDCValidationCase(True, "id", None, "url", "groups", "user", "admin", False),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"missing url",
|
||||||
|
OIDCValidationCase(True, "id", "secret", None, "groups", "user", "admin", False),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"all good no groups",
|
||||||
|
OIDCValidationCase(True, "id", "secret", "url", None, None, None, True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"all good with groups",
|
||||||
|
OIDCValidationCase(True, "id", "secret", "url", "groups", "user", "admin", True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
oidc_cases = [x[1] for x in oidc_validation_cases]
|
||||||
|
oidc_cases_ids = [x[0] for x in oidc_validation_cases]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", oidc_cases, ids=oidc_cases_ids)
|
||||||
|
def test_oidc_settings_validation(data: OIDCValidationCase, monkeypatch: pytest.MonkeyPatch):
|
||||||
|
for setting in data.settings:
|
||||||
|
if setting.value is not None:
|
||||||
|
monkeypatch.setenv(setting.name, setting.value)
|
||||||
|
else:
|
||||||
|
monkeypatch.delenv(setting.name, raising=False)
|
||||||
|
|
||||||
|
get_app_settings.cache_clear()
|
||||||
|
app_settings = get_app_settings()
|
||||||
|
|
||||||
|
assert app_settings.OIDC_READY is data.is_valid
|
||||||
|
|
||||||
|
|
||||||
|
def test_sensitive_settings_mask(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
sensitive_settings = [
|
||||||
|
"LDAP_QUERY_PASSWORD",
|
||||||
|
"OPENAI_API_KEY",
|
||||||
|
"SMTP_USER",
|
||||||
|
"SMTP_PASSWORD",
|
||||||
|
"OIDC_CLIENT_SECRET",
|
||||||
|
]
|
||||||
|
for setting in sensitive_settings:
|
||||||
|
monkeypatch.setenv(setting, "super_secret")
|
||||||
|
|
||||||
|
get_app_settings.cache_clear()
|
||||||
|
app_settings = get_app_settings()
|
||||||
|
settings = app_settings.model_dump()
|
||||||
|
settings_json = json.loads(app_settings.model_dump_json())
|
||||||
|
|
||||||
|
for setting in sensitive_settings:
|
||||||
|
assert settings[setting] == "*****"
|
||||||
|
assert settings_json[setting] == "*****"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import ldap
|
import ldap
|
||||||
|
import pytest
|
||||||
from pytest import MonkeyPatch
|
from pytest import MonkeyPatch
|
||||||
|
|
||||||
from mealie.core import security
|
from mealie.core import security
|
||||||
@@ -13,6 +14,7 @@ from mealie.core.security.providers.credentials_provider import (
|
|||||||
from mealie.core.security.providers.ldap_provider import LDAPProvider
|
from mealie.core.security.providers.ldap_provider import LDAPProvider
|
||||||
from mealie.db.db_setup import session_context
|
from mealie.db.db_setup import session_context
|
||||||
from mealie.db.models.users.users import AuthMethod
|
from mealie.db.models.users.users import AuthMethod
|
||||||
|
from mealie.repos.repository_factory import AllRepositories
|
||||||
from mealie.schema.user.auth import CredentialsRequestForm
|
from mealie.schema.user.auth import CredentialsRequestForm
|
||||||
from mealie.schema.user.user import PrivateUser
|
from mealie.schema.user.user import PrivateUser
|
||||||
from tests.utils import random_string
|
from tests.utils import random_string
|
||||||
@@ -92,7 +94,7 @@ class LdapConnMock:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def setup_env(monkeypatch: MonkeyPatch):
|
def setup_env(monkeypatch: MonkeyPatch, **kwargs):
|
||||||
user = random_string(10)
|
user = random_string(10)
|
||||||
mail = random_string(10)
|
mail = random_string(10)
|
||||||
name = random_string(10)
|
name = random_string(10)
|
||||||
@@ -140,11 +142,55 @@ def test_ldap_user_creation(monkeypatch: MonkeyPatch):
|
|||||||
provider = get_provider(session, user, password)
|
provider = get_provider(session, user, password)
|
||||||
result = provider.get_user()
|
result = provider.get_user()
|
||||||
|
|
||||||
|
app_settings = get_app_settings()
|
||||||
|
|
||||||
assert result
|
assert result
|
||||||
assert result.username == user
|
assert result.username == user
|
||||||
assert result.email == mail
|
assert result.email == mail
|
||||||
assert result.full_name == name
|
assert result.full_name == name
|
||||||
assert result.admin is False
|
assert result.admin is False
|
||||||
|
assert result.group == app_settings.DEFAULT_GROUP
|
||||||
|
assert result.household == app_settings.DEFAULT_HOUSEHOLD
|
||||||
|
assert result.auth_method == AuthMethod.LDAP
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("valid_group", [True, False])
|
||||||
|
@pytest.mark.parametrize("valid_household", [True, False])
|
||||||
|
def test_ldap_user_creation_invalid_group_or_household(
|
||||||
|
unfiltered_database: AllRepositories, monkeypatch: MonkeyPatch, valid_group: bool, valid_household: bool
|
||||||
|
):
|
||||||
|
user, mail, name, password, query_bind, query_password = setup_env(monkeypatch)
|
||||||
|
if not valid_group:
|
||||||
|
monkeypatch.setenv("DEFAULT_GROUP", random_string())
|
||||||
|
if not valid_household:
|
||||||
|
monkeypatch.setenv("DEFAULT_HOUSEHOLD", random_string())
|
||||||
|
|
||||||
|
def ldap_initialize_mock(url):
|
||||||
|
assert url == ""
|
||||||
|
return LdapConnMock(user, password, False, query_bind, query_password, mail, name)
|
||||||
|
|
||||||
|
monkeypatch.setattr(ldap, "initialize", ldap_initialize_mock)
|
||||||
|
|
||||||
|
get_app_settings.cache_clear()
|
||||||
|
|
||||||
|
with session_context() as session:
|
||||||
|
provider = get_provider(session, user, password)
|
||||||
|
try:
|
||||||
|
result = provider.get_user()
|
||||||
|
except ValueError:
|
||||||
|
result = None
|
||||||
|
|
||||||
|
if valid_group and valid_household:
|
||||||
|
assert result
|
||||||
|
else:
|
||||||
|
assert not result
|
||||||
|
|
||||||
|
# check if the user exists in the db
|
||||||
|
user = unfiltered_database.users.get_by_username(user)
|
||||||
|
if valid_group and valid_household:
|
||||||
|
assert user
|
||||||
|
else:
|
||||||
|
assert not user
|
||||||
|
|
||||||
|
|
||||||
def test_ldap_user_creation_fail(monkeypatch: MonkeyPatch):
|
def test_ldap_user_creation_fail(monkeypatch: MonkeyPatch):
|
||||||
|
|||||||
Reference in New Issue
Block a user