mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
feat(frontend): ✨ Fix scheduler, forgot password flow, and minor bug fixes (#725)
* feat(frontend): 💄 add recipe title * fix(frontend): 🐛 fixes #722 side-bar issue * feat(frontend): ✨ Add page titles to all pages * minor cleanup * refactor(backend): ♻️ rewrite scheduler to be more modulare and work * feat(frontend): ✨ start password reset functionality * refactor(backend): ♻️ refactor application settings to facilitate dependency injection * refactor(backend): 🔥 remove RECIPE_SETTINGS env variables in favor of group settings * formatting * refactor(backend): ♻️ align naming convention * feat(backend): ✨ password reset * test(backend): ✅ password reset * feat(frontend): ✨ self-service password reset * purge password schedule * update user creation for tests Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
parent
d1f0441252
commit
2e9026f9ea
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,6 +3,7 @@
|
|||||||
*__pycache__/
|
*__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
*.temp
|
||||||
|
|
||||||
# frontend/.env.development
|
# frontend/.env.development
|
||||||
docs/site/
|
docs/site/
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from mealie.app import app
|
from mealie.app import app
|
||||||
from mealie.core.config import DATA_DIR
|
from mealie.core.config import determine_data_dir
|
||||||
|
|
||||||
|
DATA_DIR = determine_data_dir()
|
||||||
|
|
||||||
"""Script to export the ReDoc documentation page into a standalone HTML file."""
|
"""Script to export the ReDoc documentation page into a standalone HTML file."""
|
||||||
|
|
||||||
|
@ -45,14 +45,6 @@ services:
|
|||||||
PGID: 1000
|
PGID: 1000
|
||||||
TZ: America/Anchorage
|
TZ: America/Anchorage
|
||||||
|
|
||||||
# Default Recipe Settings
|
|
||||||
RECIPE_PUBLIC: true
|
|
||||||
RECIPE_SHOW_NUTRITION: true
|
|
||||||
RECIPE_SHOW_ASSETS: true
|
|
||||||
RECIPE_LANDSCAPE_VIEW: true
|
|
||||||
RECIPE_DISABLE_COMMENTS: false
|
|
||||||
RECIPE_DISABLE_AMOUNT: false
|
|
||||||
|
|
||||||
# Gunicorn
|
# Gunicorn
|
||||||
WEB_CONCURRENCY: 2
|
WEB_CONCURRENCY: 2
|
||||||
# WORKERS_PER_CORE: 0.5
|
# WORKERS_PER_CORE: 0.5
|
||||||
@ -89,14 +81,6 @@ services:
|
|||||||
POSTGRES_PORT: 5432
|
POSTGRES_PORT: 5432
|
||||||
POSTGRES_DB: mealie
|
POSTGRES_DB: mealie
|
||||||
|
|
||||||
# Default Recipe Settings
|
|
||||||
RECIPE_PUBLIC: true
|
|
||||||
RECIPE_SHOW_NUTRITION: true
|
|
||||||
RECIPE_SHOW_ASSETS: true
|
|
||||||
RECIPE_LANDSCAPE_VIEW: true
|
|
||||||
RECIPE_DISABLE_COMMENTS: false
|
|
||||||
RECIPE_DISABLE_AMOUNT: false
|
|
||||||
|
|
||||||
# Gunicorn
|
# Gunicorn
|
||||||
WEB_CONCURRENCY: 2
|
WEB_CONCURRENCY: 2
|
||||||
# WORKERS_PER_CORE: 0.5
|
# WORKERS_PER_CORE: 0.5
|
||||||
|
@ -2,20 +2,17 @@ import { BaseAPI } from "./_base";
|
|||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
base: "/api/admin/email",
|
base: "/api/admin/email",
|
||||||
|
forgotPassword: "/api/users/forgot-password",
|
||||||
|
|
||||||
invitation: "/api/groups/invitations/email",
|
invitation: "/api/groups/invitations/email",
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CheckEmailResponse {
|
export interface EmailResponse {
|
||||||
ready: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestEmailResponse {
|
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TestEmailPayload {
|
export interface EmailPayload {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,21 +21,16 @@ export interface InvitationEmail {
|
|||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InvitationEmailResponse {
|
|
||||||
success: boolean;
|
|
||||||
error: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EmailAPI extends BaseAPI {
|
export class EmailAPI extends BaseAPI {
|
||||||
check() {
|
test(payload: EmailPayload) {
|
||||||
return this.requests.get<CheckEmailResponse>(routes.base);
|
return this.requests.post<EmailResponse>(routes.base, payload);
|
||||||
}
|
|
||||||
|
|
||||||
test(payload: TestEmailPayload) {
|
|
||||||
return this.requests.post<TestEmailResponse>(routes.base, payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendInvitation(payload: InvitationEmail) {
|
sendInvitation(payload: InvitationEmail) {
|
||||||
return this.requests.post<InvitationEmailResponse>(routes.invitation, payload);
|
return this.requests.post<EmailResponse>(routes.invitation, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendForgotPassword(payload: EmailPayload) {
|
||||||
|
return this.requests.post<EmailResponse>(routes.forgotPassword, payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,12 +16,20 @@ interface ResponseToken {
|
|||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PasswordResetPayload {
|
||||||
|
token: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
passwordConfirm: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Code
|
// Code
|
||||||
|
|
||||||
const prefix = "/api";
|
const prefix = "/api";
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
usersSelf: `${prefix}/users/self`,
|
usersSelf: `${prefix}/users/self`,
|
||||||
|
passwordReset: `${prefix}/users/reset-password`,
|
||||||
users: `${prefix}/users`,
|
users: `${prefix}/users`,
|
||||||
|
|
||||||
usersIdImage: (id: string) => `${prefix}/users/${id}/image`,
|
usersIdImage: (id: string) => `${prefix}/users/${id}/image`,
|
||||||
@ -55,10 +63,6 @@ export class UserApi extends BaseCRUDAPI<UserOut, UserIn> {
|
|||||||
return await this.requests.put(routes.usersIdPassword(id), changePassword);
|
return await this.requests.put(routes.usersIdPassword(id), changePassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetPassword(id: string) {
|
|
||||||
return await this.requests.post(routes.usersIdResetPassword(id), {});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createAPIToken(tokenName: CreateAPIToken) {
|
async createAPIToken(tokenName: CreateAPIToken) {
|
||||||
return await this.requests.post<ResponseToken>(routes.usersApiTokens, tokenName);
|
return await this.requests.post<ResponseToken>(routes.usersApiTokens, tokenName);
|
||||||
}
|
}
|
||||||
@ -71,4 +75,8 @@ export class UserApi extends BaseCRUDAPI<UserOut, UserIn> {
|
|||||||
if (!id || id === undefined) return;
|
if (!id || id === undefined) return;
|
||||||
return `/api/users/${id}/image`;
|
return `/api/users/${id}/image`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async resetPassword(payload: PasswordResetPayload) {
|
||||||
|
return await this.requests.post(routes.passwordReset, payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useAsync, ref, reactive } from "@nuxtjs/composition-api";
|
import { useAsync, ref } from "@nuxtjs/composition-api";
|
||||||
import { useApiSingleton } from "~/composables/use-api";
|
import { useApiSingleton } from "~/composables/use-api";
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useRoute, WritableComputedRef, computed } from "@nuxtjs/composition-api";
|
import { useRoute, WritableComputedRef, computed, nextTick, useRouter } from "@nuxtjs/composition-api";
|
||||||
|
|
||||||
export function useRouterQuery(query: string) {
|
export function useRouterQuery(query: string) {
|
||||||
const router = useRoute();
|
const router = useRoute();
|
||||||
@ -6,6 +6,7 @@ export function useRouterQuery(query: string) {
|
|||||||
|
|
||||||
const param: WritableComputedRef<string> = computed({
|
const param: WritableComputedRef<string> = computed({
|
||||||
get(): string {
|
get(): string {
|
||||||
|
console.log("Get Query Change");
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return router.value?.query[query] || "";
|
return router.value?.query[query] || "";
|
||||||
},
|
},
|
||||||
@ -16,3 +17,24 @@ export function useRouterQuery(query: string) {
|
|||||||
|
|
||||||
return param;
|
return param;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useRouteQuery<T extends string | string[]>(name: string, defaultValue?: T) {
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return computed<any>({
|
||||||
|
get() {
|
||||||
|
console.log("Getter");
|
||||||
|
const data = route.value.query[name];
|
||||||
|
if (data == null) return defaultValue ?? null;
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
nextTick(() => {
|
||||||
|
console.log("Setter");
|
||||||
|
// @ts-ignore
|
||||||
|
router.value.replace({ query: { ...route.value.query, [name]: v } });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -387,7 +387,8 @@
|
|||||||
"test-webhooks": "Test Webhooks",
|
"test-webhooks": "Test Webhooks",
|
||||||
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
|
"the-urls-listed-below-will-recieve-webhooks-containing-the-recipe-data-for-the-meal-plan-on-its-scheduled-day-currently-webhooks-will-execute-at": "The URLs listed below will receive webhooks containing the recipe data for the meal plan on it's scheduled day. Currently Webhooks will execute at",
|
||||||
"webhook-url": "Webhook URL",
|
"webhook-url": "Webhook URL",
|
||||||
"webhooks-caps": "WEBHOOKS"
|
"webhooks-caps": "WEBHOOKS",
|
||||||
|
"webhooks": "Webhooks"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shopping-list": {
|
"shopping-list": {
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
|
import { defineComponent, ref, useContext, onMounted } from "@nuxtjs/composition-api";
|
||||||
import AppHeader from "@/components/Layout/AppHeader.vue";
|
import AppHeader from "@/components/Layout/AppHeader.vue";
|
||||||
import AppSidebar from "@/components/Layout/AppSidebar.vue";
|
import AppSidebar from "@/components/Layout/AppSidebar.vue";
|
||||||
import TheSnackbar from "~/components/Layout/TheSnackbar.vue";
|
import TheSnackbar from "~/components/Layout/TheSnackbar.vue";
|
||||||
@ -38,9 +38,12 @@ export default defineComponent({
|
|||||||
auth: true,
|
auth: true,
|
||||||
setup() {
|
setup() {
|
||||||
// @ts-ignore - $globals not found in type definition
|
// @ts-ignore - $globals not found in type definition
|
||||||
const { $globals, i18n } = useContext();
|
const { $globals, i18n, $vuetify } = useContext();
|
||||||
|
|
||||||
const sidebar = ref(null);
|
const sidebar = ref<boolean | null>(null);
|
||||||
|
onMounted(() => {
|
||||||
|
sidebar.value = !$vuetify.breakpoint.md;
|
||||||
|
});
|
||||||
|
|
||||||
const topLinks = [
|
const topLinks = [
|
||||||
{
|
{
|
||||||
|
@ -9,9 +9,8 @@
|
|||||||
secondary-header="Cookbooks"
|
secondary-header="Cookbooks"
|
||||||
:secondary-links="cookbookLinks || []"
|
:secondary-links="cookbookLinks || []"
|
||||||
:bottom-links="isAdmin ? bottomLink : []"
|
:bottom-links="isAdmin ? bottomLink : []"
|
||||||
@input="sidebar = !sidebar"
|
|
||||||
>
|
>
|
||||||
<v-menu offset-y nudge-bottom="5" open-on-hover close-delay="50" nudge-right="15">
|
<v-menu offset-y nudge-bottom="5" close-delay="50" nudge-right="15">
|
||||||
<template #activator="{ on, attrs }">
|
<template #activator="{ on, attrs }">
|
||||||
<v-btn rounded large class="ml-2 mt-3" v-bind="attrs" v-on="on">
|
<v-btn rounded large class="ml-2 mt-3" v-bind="attrs" v-on="on">
|
||||||
<v-icon left large color="primary">
|
<v-icon left large color="primary">
|
||||||
@ -62,7 +61,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api";
|
import { computed, defineComponent, onMounted, ref, useContext } from "@nuxtjs/composition-api";
|
||||||
import AppHeader from "@/components/Layout/AppHeader.vue";
|
import AppHeader from "@/components/Layout/AppHeader.vue";
|
||||||
import AppSidebar from "@/components/Layout/AppSidebar.vue";
|
import AppSidebar from "@/components/Layout/AppSidebar.vue";
|
||||||
import TheSnackbar from "@/components/Layout/TheSnackbar.vue";
|
import TheSnackbar from "@/components/Layout/TheSnackbar.vue";
|
||||||
@ -84,6 +83,12 @@ export default defineComponent({
|
|||||||
console.log("toggleDark");
|
console.log("toggleDark");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sidebar = ref<Boolean | null>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
sidebar.value = !$vuetify.breakpoint.md;
|
||||||
|
});
|
||||||
|
|
||||||
const cookbookLinks = computed(() => {
|
const cookbookLinks = computed(() => {
|
||||||
if (!cookbooks.value) return [];
|
if (!cookbooks.value) return [];
|
||||||
return cookbooks.value.map((cookbook) => {
|
return cookbooks.value.map((cookbook) => {
|
||||||
@ -94,11 +99,10 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return { cookbookLinks, isAdmin, toggleDark };
|
return { cookbookLinks, isAdmin, toggleDark, sidebar };
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
sidebar: null,
|
|
||||||
createLinks: [
|
createLinks: [
|
||||||
{
|
{
|
||||||
icon: this.$globals.icons.link,
|
icon: this.$globals.icons.link,
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
layout: "empty",
|
layout: "basic",
|
||||||
props: {
|
props: {
|
||||||
error: {
|
error: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
// Global page headers: https://go.nuxtjs.dev/config-head
|
// Global page headers: https://go.nuxtjs.dev/config-head
|
||||||
head: {
|
head: {
|
||||||
titleTemplate: "%s - Mealie",
|
titleTemplate: "%s | Mealie",
|
||||||
title: "Home",
|
title: "Home",
|
||||||
meta: [
|
meta: [
|
||||||
{ charset: "utf-8" },
|
{ charset: "utf-8" },
|
||||||
|
@ -99,6 +99,11 @@ export default defineComponent({
|
|||||||
appInfo,
|
appInfo,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("about.about") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -146,6 +146,11 @@ export default defineComponent({
|
|||||||
backupsFileNameDownload,
|
backupsFileNameDownload,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("sidebar.backups") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -146,6 +146,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
return { statistics, events, deleteEvents, deleteEvent };
|
return { statistics, events, deleteEvents, deleteEvent };
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("sidebar.dashboard") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -111,5 +111,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
return { ...toRefs(state), groups, refreshAllGroups, deleteGroup, createGroup };
|
return { ...toRefs(state), groups, refreshAllGroups, deleteGroup, createGroup };
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("group.manage-groups") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -153,6 +153,11 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("sidebar.manage-users") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateUser(userData: any) {
|
updateUser(userData: any) {
|
||||||
this.updateMode = true;
|
this.updateMode = true;
|
||||||
|
@ -17,6 +17,11 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("settings.migrations") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -156,6 +156,11 @@ export default defineComponent({
|
|||||||
testEmail,
|
testEmail,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("settings.site-settings") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -12,6 +12,11 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("sidebar.categories") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -113,6 +113,11 @@ export default defineComponent({
|
|||||||
workingFoodData,
|
workingFoodData,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: "Foods",
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -215,6 +215,11 @@ export default defineComponent({
|
|||||||
notificationTypes,
|
notificationTypes,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("events.notification") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -12,6 +12,11 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("settings.organize") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -12,6 +12,11 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("sidebar.tags") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -115,6 +115,11 @@ export default defineComponent({
|
|||||||
workingUnitData,
|
workingUnitData,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: "Units",
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useRoute, ref } from "@nuxtjs/composition-api";
|
import { defineComponent, useRoute, ref, useMeta } from "@nuxtjs/composition-api";
|
||||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useCookbook } from "~/composables/use-group-cookbooks";
|
import { useCookbook } from "~/composables/use-group-cookbooks";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -37,11 +37,18 @@ export default defineComponent({
|
|||||||
|
|
||||||
const book = getOne(slug);
|
const book = getOne(slug);
|
||||||
|
|
||||||
|
useMeta(() => {
|
||||||
|
return {
|
||||||
|
title: book?.value?.name || "Cookbook",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
book,
|
book,
|
||||||
tab,
|
tab,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head: {}, // Must include for useMeta
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
86
frontend/pages/forgot-password.vue
Normal file
86
frontend/pages/forgot-password.vue
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<v-container fill-height fluid class="d-flex justify-center align-center">
|
||||||
|
<v-card color="background d-flex flex-column align-center" flat width="600px">
|
||||||
|
<v-card-title class="headline justify-center"> Forgot Password </v-card-title>
|
||||||
|
<BaseDivider />
|
||||||
|
<v-card-text>
|
||||||
|
<v-form @submit.prevent="requestLink()">
|
||||||
|
<v-text-field
|
||||||
|
v-model="email"
|
||||||
|
filled
|
||||||
|
rounded
|
||||||
|
autofocus
|
||||||
|
class="rounded-lg"
|
||||||
|
name="login"
|
||||||
|
:label="$t('user.email')"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<p class="text-center">Please enter your email address and we will send you a link to reset your password.</p>
|
||||||
|
<v-card-actions class="justify-center">
|
||||||
|
<div class="max-button">
|
||||||
|
<v-btn :loading="loading" color="primary" type="submit" large rounded class="rounded-xl" block>
|
||||||
|
<v-icon left>
|
||||||
|
{{ $globals.icons.email }}
|
||||||
|
</v-icon>
|
||||||
|
{{ $t("user.reset-password") }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-btn class="mx-auto" text nuxt to="/login"> {{ $t("user.login") }} </v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, toRefs, reactive } from "@nuxtjs/composition-api";
|
||||||
|
import { useApiSingleton } from "~/composables/use-api";
|
||||||
|
import { alert } from "~/composables/use-toast";
|
||||||
|
export default defineComponent({
|
||||||
|
layout: "basic",
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const state = reactive({
|
||||||
|
email: "",
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = useApiSingleton();
|
||||||
|
|
||||||
|
async function requestLink() {
|
||||||
|
state.loading = true;
|
||||||
|
// TODO: Fix Response to send meaningful error
|
||||||
|
const { response } = await api.email.sendForgotPassword({ email: state.email });
|
||||||
|
|
||||||
|
if (response?.status === 200) {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = false;
|
||||||
|
alert.success("Link successfully sent");
|
||||||
|
} else {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = true;
|
||||||
|
alert.error("Email failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestLink,
|
||||||
|
...toRefs(state),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("user.login") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css">
|
||||||
|
.max-button {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -18,9 +18,7 @@ export default defineComponent({
|
|||||||
components: { RecipeCardSection },
|
components: { RecipeCardSection },
|
||||||
setup() {
|
setup() {
|
||||||
const { assignSorted } = useRecipes(false);
|
const { assignSorted } = useRecipes(false);
|
||||||
|
|
||||||
useStaticRoutes();
|
useStaticRoutes();
|
||||||
|
|
||||||
return { recentRecipes, assignSorted };
|
return { recentRecipes, assignSorted };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container fill-height fluid class="d-flex justify-center align-center">
|
<v-container fill-height fluid class="d-flex justify-center align-center">
|
||||||
<v-card color="background d-flex flex-column align-center" flat width="600px">
|
<v-card tag="section" color="background d-flex flex-column align-center" flat width="600px">
|
||||||
<svg
|
<svg
|
||||||
id="bbc88faa-5a3b-49cf-bdbb-6c9ab11be594"
|
id="bbc88faa-5a3b-49cf-bdbb-6c9ab11be594"
|
||||||
data-name="Layer 1"
|
data-name="Layer 1"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 728 754.88525"
|
viewBox="0 0 728 754.88525"
|
||||||
style="max-height: 200px"
|
style="max-height: 100px"
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
@ -182,8 +182,11 @@
|
|||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-btn v-if="allowSignup" rounded class="mx-auto" text to="/register"> {{ $t("user.register") }} </v-btn>
|
<v-card-actions>
|
||||||
<v-btn v-else class="mx-auto" text disabled> {{ $t("user.invite-only") }} </v-btn>
|
<v-btn v-if="allowSignup" text to="/register"> {{ $t("user.register") }} </v-btn>
|
||||||
|
<v-btn v-else text disabled> {{ $t("user.invite-only") }} </v-btn>
|
||||||
|
<v-btn class="mr-auto" text to="/forgot-password"> {{ $t("user.reset-password") }} </v-btn>
|
||||||
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
@ -223,10 +226,15 @@ export default defineComponent({
|
|||||||
authenticate,
|
authenticate,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("user.login") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
.max-button {
|
.max-button {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
@ -309,6 +309,11 @@ export default defineComponent({
|
|||||||
days,
|
days,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("meal-plan.dinner-this-week") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -9,6 +9,11 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("meal-plan.dinner-this-week") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -397,6 +397,34 @@ export default defineComponent({
|
|||||||
scale: 1,
|
scale: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===============================================================
|
||||||
|
// Metadata
|
||||||
|
|
||||||
|
const structuredData = computed(() => {
|
||||||
|
return {
|
||||||
|
"@context": "http://schema.org",
|
||||||
|
"@type": "Recipe",
|
||||||
|
...recipe.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useMeta(() => {
|
||||||
|
return {
|
||||||
|
title: recipe?.value?.name || "Recipe",
|
||||||
|
// @ts-ignore
|
||||||
|
mainImage: recipeImage(recipe?.value?.image),
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
hid: "description",
|
||||||
|
name: "description",
|
||||||
|
content: recipe?.value?.description || "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
__dangerouslyDisableSanitizers: ["script"],
|
||||||
|
script: [{ innerHTML: JSON.stringify(structuredData), type: "application/ld+json" }],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scaledYield,
|
scaledYield,
|
||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
|
@ -214,6 +214,11 @@ export default defineComponent({
|
|||||||
validators,
|
validators,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("general.create") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
// Computed State is used because of the limitation of vue-composition-api in v2.0
|
// Computed State is used because of the limitation of vue-composition-api in v2.0
|
||||||
computed: {
|
computed: {
|
||||||
tab: {
|
tab: {
|
||||||
|
@ -47,6 +47,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
return { recipes, infiniteScroll, loading };
|
return { recipes, infiniteScroll, loading };
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("page.all-recipes") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -29,6 +29,11 @@ export default defineComponent({
|
|||||||
}, slug);
|
}, slug);
|
||||||
return { category };
|
return { category };
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("sidebar.categories") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
assignSorted(val: Array<Recipe>) {
|
assignSorted(val: Array<Recipe>) {
|
||||||
if (this.category) {
|
if (this.category) {
|
||||||
|
@ -59,6 +59,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
return { categories, api, categoriesByLetter };
|
return { categories, api, categoriesByLetter };
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("sidebar.categories") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -29,6 +29,11 @@ export default defineComponent({
|
|||||||
}, slug);
|
}, slug);
|
||||||
return { tag };
|
return { tag };
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("sidebar.tags") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
assignSorted(val: Array<Recipe>) {
|
assignSorted(val: Array<Recipe>) {
|
||||||
if (this.tag) {
|
if (this.tag) {
|
||||||
|
@ -59,6 +59,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
return { tags, api, tagsByLetter };
|
return { tags, api, tagsByLetter };
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("sidebar.tags") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -174,5 +174,10 @@ export default defineComponent({
|
|||||||
register,
|
register,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("user.register") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
140
frontend/pages/reset-password.vue
Normal file
140
frontend/pages/reset-password.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<v-container fill-height fluid class="d-flex justify-center align-center">
|
||||||
|
<v-card color="background d-flex flex-column align-center" flat width="600px">
|
||||||
|
<v-card-title class="headline justify-center"> Reset Password </v-card-title>
|
||||||
|
<BaseDivider />
|
||||||
|
<v-card-text>
|
||||||
|
<v-form @submit.prevent="requestLink()">
|
||||||
|
<v-text-field
|
||||||
|
v-model="email"
|
||||||
|
:prepend-icon="$globals.icons.email"
|
||||||
|
filled
|
||||||
|
rounded
|
||||||
|
autofocus
|
||||||
|
class="rounded-lg"
|
||||||
|
name="login"
|
||||||
|
:label="$t('user.email')"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="password"
|
||||||
|
filled
|
||||||
|
rounded
|
||||||
|
class="rounded-lg"
|
||||||
|
:prepend-icon="$globals.icons.lock"
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
:rules="[validators.required]"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="passwordConfirm"
|
||||||
|
filled
|
||||||
|
rounded
|
||||||
|
validate-on-blur
|
||||||
|
class="rounded-lg"
|
||||||
|
:prepend-icon="$globals.icons.lock"
|
||||||
|
name="password"
|
||||||
|
label="Confirm Password"
|
||||||
|
type="password"
|
||||||
|
:rules="[validators.required, passwordMatch]"
|
||||||
|
/>
|
||||||
|
<p class="text-center">Please enter your new password.</p>
|
||||||
|
<v-card-actions class="justify-center">
|
||||||
|
<div class="max-button">
|
||||||
|
<v-btn
|
||||||
|
:loading="loading"
|
||||||
|
color="primary"
|
||||||
|
:disabled="token === ''"
|
||||||
|
type="submit"
|
||||||
|
large
|
||||||
|
rounded
|
||||||
|
class="rounded-xl"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
<v-icon left>
|
||||||
|
{{ $globals.icons.lock }}
|
||||||
|
</v-icon>
|
||||||
|
{{ token === "" ? "Token Required" : $t("user.reset-password") }}
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-btn class="mx-auto" text nuxt to="/login"> {{ $t("user.login") }} </v-btn>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, toRefs, reactive } from "@nuxtjs/composition-api";
|
||||||
|
import { useApiSingleton } from "~/composables/use-api";
|
||||||
|
import { alert } from "~/composables/use-toast";
|
||||||
|
import { validators } from "@/composables/use-validators";
|
||||||
|
import { useRouteQuery } from "~/composables/use-router";
|
||||||
|
export default defineComponent({
|
||||||
|
layout: "basic",
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const state = reactive({
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
passwordConfirm: "",
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordMatch = () => state.password === state.passwordConfirm || "Passwords do not match";
|
||||||
|
|
||||||
|
// ===================
|
||||||
|
// Token Getter
|
||||||
|
const token = useRouteQuery("token", "");
|
||||||
|
|
||||||
|
// ===================
|
||||||
|
// API
|
||||||
|
const api = useApiSingleton();
|
||||||
|
async function requestLink() {
|
||||||
|
state.loading = true;
|
||||||
|
// TODO: Fix Response to send meaningful error
|
||||||
|
const { response } = await api.users.resetPassword({
|
||||||
|
token: token.value,
|
||||||
|
email: state.email,
|
||||||
|
password: state.password,
|
||||||
|
passwordConfirm: state.passwordConfirm,
|
||||||
|
});
|
||||||
|
|
||||||
|
state.loading = false;
|
||||||
|
|
||||||
|
if (response?.status === 200) {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = false;
|
||||||
|
alert.success("Password Reset Successful");
|
||||||
|
} else {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = true;
|
||||||
|
alert.error("Something Went Wrong");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
passwordMatch,
|
||||||
|
token,
|
||||||
|
requestLink,
|
||||||
|
validators,
|
||||||
|
...toRefs(state),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("user.login") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css">
|
||||||
|
.max-button {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -110,6 +110,11 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("search.search"),
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
searchString: {
|
searchString: {
|
||||||
set(q) {
|
set(q) {
|
||||||
|
@ -9,6 +9,11 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("shopping-list.shopping-list") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { defineComponent } from "@nuxtjs/composition-api";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("shopping-list.shopping-list") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
export default defineComponent({
|
<style scoped>
|
||||||
setup() {
|
</style>
|
||||||
return {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
@ -9,6 +9,11 @@ export default defineComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("general.favorites") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -3,13 +3,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { defineComponent } from "@nuxtjs/composition-api";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
return {}
|
return {};
|
||||||
}
|
},
|
||||||
})
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("settings.profile") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -63,6 +63,11 @@ export default defineComponent({
|
|||||||
actions,
|
actions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("settings.pages") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -133,6 +133,11 @@ export default defineComponent({
|
|||||||
allDays,
|
allDays,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("group.group") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -109,5 +109,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
return { members, headers, setPermissions };
|
return { members, headers, setPermissions };
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: "Members",
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
@ -64,5 +64,10 @@ export default defineComponent({
|
|||||||
actions,
|
actions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("settings.webhooks.webhooks") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
@ -126,6 +126,11 @@ export default defineComponent({
|
|||||||
|
|
||||||
return { createToken, deleteToken, copyToken, createdToken, loading, name, user, resetCreate };
|
return { createToken, deleteToken, copyToken, createdToken, loading, name, user, resetCreate };
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("settings.token.api-tokens") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -7,8 +7,6 @@
|
|||||||
<template #title> Your Profile Settings </template>
|
<template #title> Your Profile Settings </template>
|
||||||
</BasePageTitle>
|
</BasePageTitle>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<ToggleState tag="article">
|
<ToggleState tag="article">
|
||||||
<template #activator="{ toggle, state }">
|
<template #activator="{ toggle, state }">
|
||||||
@ -161,6 +159,11 @@ export default defineComponent({
|
|||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("settings.profile") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async changePassword() {
|
async changePassword() {
|
||||||
|
@ -193,6 +193,11 @@ export default defineComponent({
|
|||||||
...toRefs(state),
|
...toRefs(state),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head() {
|
||||||
|
return {
|
||||||
|
title: this.$t("settings.profile") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "@nuxtjs/composition-api";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
setup() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
</style>
|
|
@ -1,113 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-container fill-height fluid class="d-flex justify-center align-start">
|
|
||||||
<v-card color="background d-flex flex-column align-center " flat width="600px">
|
|
||||||
<svg
|
|
||||||
id="b76bd6b3-ad77-41ff-b778-1d1d054fe577"
|
|
||||||
data-name="Layer 1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
style="max-height: 300px"
|
|
||||||
viewBox="0 0 570 511.67482"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M879.99927,389.83741a.99678.99678,0,0,1-.5708-.1792L602.86963,197.05469a5.01548,5.01548,0,0,0-5.72852.00977L322.57434,389.65626a1.00019,1.00019,0,0,1-1.14868-1.6377l274.567-192.5918a7.02216,7.02216,0,0,1,8.02-.01318l276.55883,192.603a1.00019,1.00019,0,0,1-.57226,1.8208Z"
|
|
||||||
transform="translate(-315 -194.16259)"
|
|
||||||
fill="#3f3d56"
|
|
||||||
/>
|
|
||||||
<polygon
|
|
||||||
points="23.264 202.502 285.276 8.319 549.276 216.319 298.776 364.819 162.776 333.819 23.264 202.502"
|
|
||||||
fill="#e6e6e6"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M489.25553,650.70367H359.81522a6.04737,6.04737,0,1,1,0-12.09473H489.25553a6.04737,6.04737,0,1,1,0,12.09473Z"
|
|
||||||
transform="translate(-315 -194.16259)"
|
|
||||||
fill="#e58325"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M406.25553,624.70367H359.81522a6.04737,6.04737,0,1,1,0-12.09473h46.44031a6.04737,6.04737,0,1,1,0,12.09473Z"
|
|
||||||
transform="translate(-315 -194.16259)"
|
|
||||||
fill="#e58325"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M603.96016,504.82207a7.56366,7.56366,0,0,1-2.86914-.562L439.5002,437.21123v-209.874a7.00817,7.00817,0,0,1,7-7h310a7.00818,7.00818,0,0,1,7,7v210.0205l-.30371.12989L606.91622,504.22734A7.61624,7.61624,0,0,1,603.96016,504.82207Z"
|
|
||||||
transform="translate(-315 -194.16259)"
|
|
||||||
fill="#fff"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M603.96016,505.32158a8.07177,8.07177,0,0,1-3.05957-.59863L439.0002,437.54521v-210.208a7.50851,7.50851,0,0,1,7.5-7.5h310a7.50851,7.50851,0,0,1,7.5,7.5V437.68779l-156.8877,66.999A8.10957,8.10957,0,0,1,603.96016,505.32158Zm-162.96-69.1123,160.66309,66.66455a6.1182,6.1182,0,0,0,4.668-.02784l155.669-66.47851V227.33721a5.50653,5.50653,0,0,0-5.5-5.5h-310a5.50653,5.50653,0,0,0-5.5,5.5Z"
|
|
||||||
transform="translate(-315 -194.16259)"
|
|
||||||
fill="#3f3d56"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M878,387.83741h-.2002L763,436.85743l-157.06982,67.07a5.06614,5.06614,0,0,1-3.88038.02L440,436.71741l-117.62012-48.8-.17968-.08H322a7.00778,7.00778,0,0,0-7,7v304a7.00779,7.00779,0,0,0,7,7H878a7.00779,7.00779,0,0,0,7-7v-304A7.00778,7.00778,0,0,0,878,387.83741Zm5,311a5.002,5.002,0,0,1-5,5H322a5.002,5.002,0,0,1-5-5v-304a5.01106,5.01106,0,0,1,4.81006-5L440,438.87739l161.28027,66.92a7.12081,7.12081,0,0,0,5.43994-.03L763,439.02741l115.2002-49.19a5.01621,5.01621,0,0,1,4.7998,5Z"
|
|
||||||
transform="translate(-315 -194.16259)"
|
|
||||||
fill="#3f3d56"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M602.345,445.30958a27.49862,27.49862,0,0,1-16.5459-5.4961l-.2959-.22217-62.311-47.70752a27.68337,27.68337,0,1,1,33.67407-43.94921l40.36035,30.94775,95.37793-124.38672a27.68235,27.68235,0,0,1,38.81323-5.12353l-.593.80517.6084-.79346a27.71447,27.71447,0,0,1,5.12353,38.81348L624.36938,434.50586A27.69447,27.69447,0,0,1,602.345,445.30958Z"
|
|
||||||
transform="translate(-315 -194.16259)"
|
|
||||||
fill="#e58325"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<v-card-title class="headline justify-center"> Request Secure Link </v-card-title>
|
|
||||||
<BaseDivider />
|
|
||||||
<v-card-text>
|
|
||||||
<v-form @submit.prevent="authenticate()">
|
|
||||||
<v-text-field
|
|
||||||
v-model="form.email"
|
|
||||||
filled
|
|
||||||
rounded
|
|
||||||
autofocus
|
|
||||||
class="rounded-lg"
|
|
||||||
prepend-icon="mdi-account"
|
|
||||||
name="login"
|
|
||||||
label="Email"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
<v-btn :loading="loggingIn" color="primary" type="submit" large rounded class="rounded-xl" block>
|
|
||||||
Submit
|
|
||||||
</v-btn>
|
|
||||||
</v-form>
|
|
||||||
</v-card-text>
|
|
||||||
<v-btn v-if="allowSignup" class="mx-auto" text to="/login"> Login </v-btn>
|
|
||||||
</v-card>
|
|
||||||
<!-- <v-col class="fill-height"> </v-col> -->
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "@nuxtjs/composition-api";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
layout: "basic",
|
|
||||||
setup() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
loggingIn: false,
|
|
||||||
form: {
|
|
||||||
email: "changeme@email.com",
|
|
||||||
password: "MyPassword",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
allowSignup(): boolean {
|
|
||||||
// @ts-ignore
|
|
||||||
return process.env.ALLOW_SIGNUP;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async authenticate() {
|
|
||||||
this.loggingIn = true;
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("username", this.form.email);
|
|
||||||
formData.append("password", this.form.password);
|
|
||||||
|
|
||||||
await this.$auth.loginWith("local", { data: formData });
|
|
||||||
this.loggingIn = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
@ -55,7 +55,7 @@ export interface Recipe {
|
|||||||
id?: number;
|
id?: number;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
image?: unknown;
|
image: string;
|
||||||
description: string;
|
description: string;
|
||||||
recipeCategory: string[];
|
recipeCategory: string[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
@ -2,15 +2,18 @@ import uvicorn
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
|
|
||||||
from mealie.core.config import APP_VERSION, settings
|
from mealie.core.config import get_app_settings
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
|
from mealie.core.settings.static import APP_VERSION
|
||||||
from mealie.routes import backup_routes, migration_routes, router, utility_routes
|
from mealie.routes import backup_routes, migration_routes, router, utility_routes
|
||||||
from mealie.routes.about import about_router
|
from mealie.routes.about import about_router
|
||||||
from mealie.routes.media import media_router
|
from mealie.routes.media import media_router
|
||||||
from mealie.routes.site_settings import settings_router
|
from mealie.routes.site_settings import settings_router
|
||||||
from mealie.services.events import create_general_event
|
from mealie.services.events import create_general_event
|
||||||
|
from mealie.services.scheduler import SchedulerRegistry, SchedulerService, tasks
|
||||||
|
|
||||||
logger = get_logger()
|
logger = get_logger()
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Mealie",
|
title="Mealie",
|
||||||
@ -24,24 +27,28 @@ app.add_middleware(GZipMiddleware, minimum_size=1000)
|
|||||||
|
|
||||||
|
|
||||||
def start_scheduler():
|
def start_scheduler():
|
||||||
return # TODO: Disable Scheduler for now
|
SchedulerService.start()
|
||||||
import mealie.services.scheduler.scheduled_jobs # noqa: F401
|
|
||||||
|
SchedulerRegistry.register_daily(
|
||||||
|
tasks.purge_events_database,
|
||||||
|
tasks.purge_group_registration,
|
||||||
|
tasks.auto_backup,
|
||||||
|
tasks.purge_password_reset_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
|
SchedulerRegistry.register_hourly()
|
||||||
|
SchedulerRegistry.register_minutely(tasks.update_group_webhooks)
|
||||||
|
|
||||||
|
logger.info(SchedulerService.scheduler.print_jobs())
|
||||||
|
|
||||||
|
|
||||||
def api_routers():
|
def api_routers():
|
||||||
# Authentication
|
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
# Recipes
|
|
||||||
app.include_router(media_router)
|
app.include_router(media_router)
|
||||||
app.include_router(about_router)
|
app.include_router(about_router)
|
||||||
# Meal Routes
|
|
||||||
# Settings Routes
|
|
||||||
app.include_router(settings_router)
|
app.include_router(settings_router)
|
||||||
# Backups/Imports Routes
|
|
||||||
app.include_router(backup_routes.router)
|
app.include_router(backup_routes.router)
|
||||||
# Migration Routes
|
|
||||||
app.include_router(migration_routes.router)
|
app.include_router(migration_routes.router)
|
||||||
# Debug routes
|
|
||||||
app.include_router(utility_routes.router)
|
app.include_router(utility_routes.router)
|
||||||
|
|
||||||
|
|
||||||
@ -51,6 +58,7 @@ api_routers()
|
|||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
def system_startup():
|
def system_startup():
|
||||||
start_scheduler()
|
start_scheduler()
|
||||||
|
|
||||||
logger.info("-----SYSTEM STARTUP----- \n")
|
logger.info("-----SYSTEM STARTUP----- \n")
|
||||||
logger.info("------APP SETTINGS------")
|
logger.info("------APP SETTINGS------")
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -64,9 +72,12 @@ def system_startup():
|
|||||||
"DB_URL", # replace by DB_URL_PUBLIC for logs
|
"DB_URL", # replace by DB_URL_PUBLIC for logs
|
||||||
"POSTGRES_USER",
|
"POSTGRES_USER",
|
||||||
"POSTGRES_PASSWORD",
|
"POSTGRES_PASSWORD",
|
||||||
|
"SMTP_USER",
|
||||||
|
"SMTP_PASSWORD",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
create_general_event("Application Startup", f"Mealie API started on port {settings.API_PORT}")
|
create_general_event("Application Startup", f"Mealie API started on port {settings.API_PORT}")
|
||||||
|
|
||||||
|
|
||||||
@ -77,6 +88,7 @@ def main():
|
|||||||
port=settings.API_PORT,
|
port=settings.API_PORT,
|
||||||
reload=True,
|
reload=True,
|
||||||
reload_dirs=["mealie"],
|
reload_dirs=["mealie"],
|
||||||
|
reload_delay=2,
|
||||||
debug=True,
|
debug=True,
|
||||||
log_level="debug",
|
log_level="debug",
|
||||||
use_colors=True,
|
use_colors=True,
|
||||||
|
@ -1,209 +1,39 @@
|
|||||||
import os
|
import os
|
||||||
import secrets
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional, Union
|
|
||||||
|
|
||||||
import dotenv
|
import dotenv
|
||||||
from pydantic import BaseSettings, Field, PostgresDsn, validator
|
|
||||||
|
|
||||||
APP_VERSION = "v1.0.0b"
|
from mealie.core.settings.settings import app_settings_constructor
|
||||||
DB_VERSION = "v1.0.0b"
|
|
||||||
|
from .settings import AppDirectories, AppSettings
|
||||||
|
from .settings.static import APP_VERSION, DB_VERSION
|
||||||
|
|
||||||
|
APP_VERSION
|
||||||
|
DB_VERSION
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
BASE_DIR = CWD.parent.parent
|
BASE_DIR = CWD.parent.parent
|
||||||
|
|
||||||
ENV = BASE_DIR.joinpath(".env")
|
ENV = BASE_DIR.joinpath(".env")
|
||||||
|
|
||||||
dotenv.load_dotenv(ENV)
|
dotenv.load_dotenv(ENV)
|
||||||
PRODUCTION = os.getenv("PRODUCTION", "True").lower() in ["true", "1"]
|
PRODUCTION = os.getenv("PRODUCTION", "True").lower() in ["true", "1"]
|
||||||
|
|
||||||
|
|
||||||
def determine_data_dir(production: bool) -> Path:
|
def determine_data_dir() -> Path:
|
||||||
global CWD
|
global PRODUCTION
|
||||||
if production:
|
global BASE_DIR
|
||||||
|
if PRODUCTION:
|
||||||
return Path("/app/data")
|
return Path("/app/data")
|
||||||
|
|
||||||
return CWD.parent.parent.joinpath("dev", "data")
|
return BASE_DIR.joinpath("dev", "data")
|
||||||
|
|
||||||
|
|
||||||
def determine_secrets(data_dir: Path, production: bool) -> str:
|
|
||||||
if not production:
|
|
||||||
return "shh-secret-test-key"
|
|
||||||
|
|
||||||
secrets_file = data_dir.joinpath(".secret")
|
|
||||||
if secrets_file.is_file():
|
|
||||||
with open(secrets_file, "r") as f:
|
|
||||||
return f.read()
|
|
||||||
else:
|
|
||||||
with open(secrets_file, "w") as f:
|
|
||||||
new_secret = secrets.token_hex(32)
|
|
||||||
f.write(new_secret)
|
|
||||||
return new_secret
|
|
||||||
|
|
||||||
|
|
||||||
# General
|
|
||||||
DATA_DIR = determine_data_dir(PRODUCTION)
|
|
||||||
|
|
||||||
|
|
||||||
class AppDirectories:
|
|
||||||
def __init__(self, cwd, data_dir) -> None:
|
|
||||||
self.DATA_DIR: Path = data_dir
|
|
||||||
self.WEB_PATH: Path = cwd.joinpath("dist")
|
|
||||||
self.IMG_DIR: Path = data_dir.joinpath("img")
|
|
||||||
self.BACKUP_DIR: Path = data_dir.joinpath("backups")
|
|
||||||
self.DEBUG_DIR: Path = data_dir.joinpath("debug")
|
|
||||||
self.MIGRATION_DIR: Path = data_dir.joinpath("migration")
|
|
||||||
self.NEXTCLOUD_DIR: Path = self.MIGRATION_DIR.joinpath("nextcloud")
|
|
||||||
self.CHOWDOWN_DIR: Path = self.MIGRATION_DIR.joinpath("chowdown")
|
|
||||||
self.TEMPLATE_DIR: Path = data_dir.joinpath("templates")
|
|
||||||
self.USER_DIR: Path = data_dir.joinpath("users")
|
|
||||||
self.RECIPE_DATA_DIR: Path = data_dir.joinpath("recipes")
|
|
||||||
self.TEMP_DIR: Path = data_dir.joinpath(".temp")
|
|
||||||
|
|
||||||
self.ensure_directories()
|
|
||||||
|
|
||||||
def ensure_directories(self):
|
|
||||||
required_dirs = [
|
|
||||||
self.IMG_DIR,
|
|
||||||
self.BACKUP_DIR,
|
|
||||||
self.DEBUG_DIR,
|
|
||||||
self.MIGRATION_DIR,
|
|
||||||
self.TEMPLATE_DIR,
|
|
||||||
self.NEXTCLOUD_DIR,
|
|
||||||
self.CHOWDOWN_DIR,
|
|
||||||
self.RECIPE_DATA_DIR,
|
|
||||||
self.USER_DIR,
|
|
||||||
]
|
|
||||||
|
|
||||||
for dir in required_dirs:
|
|
||||||
dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
app_dirs = AppDirectories(CWD, DATA_DIR)
|
|
||||||
|
|
||||||
|
|
||||||
def determine_sqlite_path(path=False, suffix=DB_VERSION) -> str:
|
|
||||||
global app_dirs
|
|
||||||
db_path = app_dirs.DATA_DIR.joinpath(f"mealie_{suffix}.db") # ! Temporary Until Alembic
|
|
||||||
|
|
||||||
if path:
|
|
||||||
return db_path
|
|
||||||
|
|
||||||
return "sqlite:///" + str(db_path.absolute())
|
|
||||||
|
|
||||||
|
|
||||||
class AppSettings(BaseSettings):
|
|
||||||
global DATA_DIR
|
|
||||||
PRODUCTION: bool = Field(True, env="PRODUCTION")
|
|
||||||
BASE_URL: str = "http://localhost:8080"
|
|
||||||
IS_DEMO: bool = False
|
|
||||||
API_PORT: int = 9000
|
|
||||||
API_DOCS: bool = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def DOCS_URL(self) -> str:
|
|
||||||
return "/docs" if self.API_DOCS else None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def REDOC_URL(self) -> str:
|
|
||||||
return "/redoc" if self.API_DOCS else None
|
|
||||||
|
|
||||||
SECRET: str = determine_secrets(DATA_DIR, PRODUCTION)
|
|
||||||
|
|
||||||
DB_ENGINE: str = "sqlite" # Optional: 'sqlite', 'postgres'
|
|
||||||
POSTGRES_USER: str = "mealie"
|
|
||||||
POSTGRES_PASSWORD: str = "mealie"
|
|
||||||
POSTGRES_SERVER: str = "postgres"
|
|
||||||
POSTGRES_PORT: str = 5432
|
|
||||||
POSTGRES_DB: str = "mealie"
|
|
||||||
|
|
||||||
DB_URL: Union[str, PostgresDsn] = None # Actual DB_URL is calculated with `assemble_db_connection`
|
|
||||||
|
|
||||||
@validator("DB_URL", pre=True)
|
|
||||||
def assemble_db_connection(cls, v: Optional[str], values: dict[str, Any]) -> Any:
|
|
||||||
engine = values.get("DB_ENGINE", "sqlite")
|
|
||||||
if engine == "postgres":
|
|
||||||
host = f"{values.get('POSTGRES_SERVER')}:{values.get('POSTGRES_PORT')}"
|
|
||||||
return PostgresDsn.build(
|
|
||||||
scheme="postgresql",
|
|
||||||
user=values.get("POSTGRES_USER"),
|
|
||||||
password=values.get("POSTGRES_PASSWORD"),
|
|
||||||
host=host,
|
|
||||||
path=f"/{values.get('POSTGRES_DB') or ''}",
|
|
||||||
)
|
|
||||||
return determine_sqlite_path()
|
|
||||||
|
|
||||||
DB_URL_PUBLIC: str = "" # hide credentials to show on logs/frontend
|
|
||||||
|
|
||||||
@validator("DB_URL_PUBLIC", pre=True)
|
|
||||||
def public_db_url(cls, v: Optional[str], values: dict[str, Any]) -> str:
|
|
||||||
url = values.get("DB_URL")
|
|
||||||
engine = values.get("DB_ENGINE", "sqlite")
|
|
||||||
if engine != "postgres":
|
|
||||||
# sqlite
|
|
||||||
return url
|
|
||||||
|
|
||||||
user = values.get("POSTGRES_USER")
|
|
||||||
password = values.get("POSTGRES_PASSWORD")
|
|
||||||
return url.replace(user, "*****", 1).replace(password, "*****", 1)
|
|
||||||
|
|
||||||
DEFAULT_GROUP: str = "Home"
|
|
||||||
DEFAULT_EMAIL: str = "changeme@email.com"
|
|
||||||
DEFAULT_PASSWORD: str = "MyPassword"
|
|
||||||
|
|
||||||
SCHEDULER_DATABASE = f"sqlite:///{app_dirs.DATA_DIR.joinpath('scheduler.db')}"
|
|
||||||
|
|
||||||
TOKEN_TIME: int = 2 # Time in Hours
|
|
||||||
|
|
||||||
# Recipe Default Settings
|
|
||||||
RECIPE_PUBLIC: bool = True
|
|
||||||
RECIPE_SHOW_NUTRITION: bool = True
|
|
||||||
RECIPE_SHOW_ASSETS: bool = True
|
|
||||||
RECIPE_LANDSCAPE_VIEW: bool = True
|
|
||||||
RECIPE_DISABLE_COMMENTS: bool = False
|
|
||||||
RECIPE_DISABLE_AMOUNT: bool = False
|
|
||||||
|
|
||||||
# ===============================================
|
|
||||||
# Email Configuration
|
|
||||||
SMTP_HOST: Optional[str]
|
|
||||||
SMTP_PORT: Optional[str] = "587"
|
|
||||||
SMTP_FROM_NAME: Optional[str] = "Mealie"
|
|
||||||
SMTP_TLS: Optional[bool] = True
|
|
||||||
SMTP_FROM_EMAIL: Optional[str]
|
|
||||||
SMTP_USER: Optional[str]
|
|
||||||
SMTP_PASSWORD: Optional[str]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def SMTP_ENABLE(self) -> bool:
|
|
||||||
"""Validates all SMTP variables are set"""
|
|
||||||
required = {
|
|
||||||
self.SMTP_HOST,
|
|
||||||
self.SMTP_PORT,
|
|
||||||
self.SMTP_FROM_NAME,
|
|
||||||
self.SMTP_TLS,
|
|
||||||
self.SMTP_FROM_EMAIL,
|
|
||||||
self.SMTP_USER,
|
|
||||||
self.SMTP_PASSWORD,
|
|
||||||
}
|
|
||||||
|
|
||||||
return "" not in required and None not in required
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_file = BASE_DIR.joinpath(".env")
|
|
||||||
env_file_encoding = "utf-8"
|
|
||||||
|
|
||||||
|
|
||||||
settings = AppSettings()
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def get_app_dirs() -> AppDirectories:
|
def get_app_dirs() -> AppDirectories:
|
||||||
global app_dirs
|
return AppDirectories(determine_data_dir())
|
||||||
return app_dirs
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def get_settings() -> AppSettings:
|
def get_app_settings() -> AppSettings:
|
||||||
global settings
|
return app_settings_constructor(env_file=ENV, production=PRODUCTION, data_dir=determine_data_dir())
|
||||||
return settings
|
|
||||||
|
@ -8,7 +8,7 @@ from fastapi.security import OAuth2PasswordBearer
|
|||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import app_dirs, settings
|
from mealie.core.config import get_app_dirs, get_app_settings
|
||||||
from mealie.db.database import get_database
|
from mealie.db.database import get_database
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.schema.user import LongLiveTokenInDB, PrivateUser, TokenData
|
from mealie.schema.user import LongLiveTokenInDB, PrivateUser, TokenData
|
||||||
@ -16,6 +16,8 @@ from mealie.schema.user import LongLiveTokenInDB, PrivateUser, TokenData
|
|||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
|
||||||
oauth2_scheme_soft_fail = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=False)
|
oauth2_scheme_soft_fail = OAuth2PasswordBearer(tokenUrl="/api/auth/token", auto_error=False)
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
|
app_dirs = get_app_dirs()
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
|
||||||
async def is_logged_in(token: str = Depends(oauth2_scheme_soft_fail), session=Depends(generate_session)) -> bool:
|
async def is_logged_in(token: str = Depends(oauth2_scheme_soft_fail), session=Depends(generate_session)) -> bool:
|
||||||
|
@ -3,9 +3,13 @@ import sys
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
from mealie.core.config import DATA_DIR
|
from mealie.core.config import determine_data_dir
|
||||||
|
|
||||||
from .config import settings
|
DATA_DIR = determine_data_dir()
|
||||||
|
|
||||||
|
from .config import get_app_settings
|
||||||
|
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
|
LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
|
||||||
DATE_FORMAT = "%d-%b-%y %H:%M:%S"
|
DATE_FORMAT = "%d-%b-%y %H:%M:%S"
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
|
import secrets
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import get_app_settings
|
||||||
from mealie.db.database import get_database
|
from mealie.db.database import get_database
|
||||||
from mealie.schema.user import PrivateUser
|
from mealie.schema.user import PrivateUser
|
||||||
|
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
|
|
||||||
@ -43,26 +46,15 @@ def authenticate_user(session, email: str, password: str) -> PrivateUser:
|
|||||||
|
|
||||||
|
|
||||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
"""Compares a plain string to a hashed password
|
"""Compares a plain string to a hashed password"""
|
||||||
|
|
||||||
Args:
|
|
||||||
plain_password (str): raw password string
|
|
||||||
hashed_password (str): hashed password from the database
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Returns True if a match return False
|
|
||||||
"""
|
|
||||||
return pwd_context.verify(plain_password, hashed_password)
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password: str) -> str:
|
def hash_password(password: str) -> str:
|
||||||
"""Takes in a raw password and hashes it. Used prior to saving
|
"""Takes in a raw password and hashes it. Used prior to saving a new password to the database."""
|
||||||
a new password to the database.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
password (str): Password String
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Hashed Password
|
|
||||||
"""
|
|
||||||
return pwd_context.hash(password)
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
def url_safe_token() -> str:
|
||||||
|
"""Generates a cryptographic token without embedded data. Used for password reset tokens and invitation tokens"""
|
||||||
|
return secrets.token_urlsafe(24)
|
||||||
|
2
mealie/core/settings/__init__.py
Normal file
2
mealie/core/settings/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .directories import *
|
||||||
|
from .settings import *
|
65
mealie/core/settings/db_providers.py
Normal file
65
mealie/core/settings/db_providers.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from abc import ABC, abstractproperty
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pydantic import BaseModel, BaseSettings, PostgresDsn
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractDBProvider(ABC):
|
||||||
|
@abstractproperty
|
||||||
|
def db_url(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_url_public(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SQLiteProvider(AbstractDBProvider, BaseModel):
|
||||||
|
data_dir: Path
|
||||||
|
prefix: str = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_path(self):
|
||||||
|
return self.data_dir / f"{self.prefix}mealie.db"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_url(self) -> str:
|
||||||
|
return "sqlite:///" + str(self.db_path.absolute())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_url_public(self) -> str:
|
||||||
|
return self.db_url
|
||||||
|
|
||||||
|
|
||||||
|
class PostgresProvider(AbstractDBProvider, BaseSettings):
|
||||||
|
POSTGRES_USER: str = "mealie"
|
||||||
|
POSTGRES_PASSWORD: str = "mealie"
|
||||||
|
POSTGRES_SERVER: str = "postgres"
|
||||||
|
POSTGRES_PORT: str = 5432
|
||||||
|
POSTGRES_DB: str = "mealie"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_url(self) -> str:
|
||||||
|
host = f"{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}"
|
||||||
|
return PostgresDsn.build(
|
||||||
|
scheme="postgresql",
|
||||||
|
user=self.POSTGRES_USER,
|
||||||
|
password=self.POSTGRES_PASSWORD,
|
||||||
|
host=host,
|
||||||
|
path=f"/{self.POSTGRES_DB or ''}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_url_public(self) -> str:
|
||||||
|
user = self.POSTGRES_USER
|
||||||
|
password = self.POSTGRES_PASSWORD
|
||||||
|
return self.db_url.replace(user, "*****", 1).replace(password, "*****", 1)
|
||||||
|
|
||||||
|
|
||||||
|
def db_provider_factory(provider_name: str, data_dir: Path, env_file: Path, env_encoding="utf-8") -> AbstractDBProvider:
|
||||||
|
if provider_name == "sqlite":
|
||||||
|
return SQLiteProvider(data_dir=data_dir)
|
||||||
|
elif provider_name == "postgres":
|
||||||
|
return PostgresProvider(_env_file=env_file, _env_file_encoding=env_encoding)
|
||||||
|
else:
|
||||||
|
return
|
34
mealie/core/settings/directories.py
Normal file
34
mealie/core/settings/directories.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class AppDirectories:
|
||||||
|
def __init__(self, data_dir) -> None:
|
||||||
|
self.DATA_DIR: Path = data_dir
|
||||||
|
self.IMG_DIR: Path = data_dir.joinpath("img")
|
||||||
|
self.BACKUP_DIR: Path = data_dir.joinpath("backups")
|
||||||
|
self.DEBUG_DIR: Path = data_dir.joinpath("debug")
|
||||||
|
self.MIGRATION_DIR: Path = data_dir.joinpath("migration")
|
||||||
|
self.NEXTCLOUD_DIR: Path = self.MIGRATION_DIR.joinpath("nextcloud")
|
||||||
|
self.CHOWDOWN_DIR: Path = self.MIGRATION_DIR.joinpath("chowdown")
|
||||||
|
self.TEMPLATE_DIR: Path = data_dir.joinpath("templates")
|
||||||
|
self.USER_DIR: Path = data_dir.joinpath("users")
|
||||||
|
self.RECIPE_DATA_DIR: Path = data_dir.joinpath("recipes")
|
||||||
|
self.TEMP_DIR: Path = data_dir.joinpath(".temp")
|
||||||
|
|
||||||
|
self.ensure_directories()
|
||||||
|
|
||||||
|
def ensure_directories(self):
|
||||||
|
required_dirs = [
|
||||||
|
self.IMG_DIR,
|
||||||
|
self.BACKUP_DIR,
|
||||||
|
self.DEBUG_DIR,
|
||||||
|
self.MIGRATION_DIR,
|
||||||
|
self.TEMPLATE_DIR,
|
||||||
|
self.NEXTCLOUD_DIR,
|
||||||
|
self.CHOWDOWN_DIR,
|
||||||
|
self.RECIPE_DATA_DIR,
|
||||||
|
self.USER_DIR,
|
||||||
|
]
|
||||||
|
|
||||||
|
for dir in required_dirs:
|
||||||
|
dir.mkdir(parents=True, exist_ok=True)
|
109
mealie/core/settings/settings.py
Normal file
109
mealie/core/settings/settings.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import secrets
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseSettings
|
||||||
|
|
||||||
|
from .db_providers import AbstractDBProvider, db_provider_factory
|
||||||
|
|
||||||
|
|
||||||
|
def determine_secrets(data_dir: Path, production: bool) -> str:
|
||||||
|
if not production:
|
||||||
|
return "shh-secret-test-key"
|
||||||
|
|
||||||
|
secrets_file = data_dir.joinpath(".secret")
|
||||||
|
if secrets_file.is_file():
|
||||||
|
with open(secrets_file, "r") as f:
|
||||||
|
return f.read()
|
||||||
|
else:
|
||||||
|
with open(secrets_file, "w") as f:
|
||||||
|
new_secret = secrets.token_hex(32)
|
||||||
|
f.write(new_secret)
|
||||||
|
return new_secret
|
||||||
|
|
||||||
|
|
||||||
|
class AppSettings(BaseSettings):
|
||||||
|
PRODUCTION: bool
|
||||||
|
BASE_URL: str = "http://localhost:8080"
|
||||||
|
IS_DEMO: bool = False
|
||||||
|
API_PORT: int = 9000
|
||||||
|
API_DOCS: bool = True
|
||||||
|
TOKEN_TIME: int = 48 # Time in Hours
|
||||||
|
SECRET: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def DOCS_URL(self) -> str:
|
||||||
|
return "/docs" if self.API_DOCS else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def REDOC_URL(self) -> str:
|
||||||
|
return "/redoc" if self.API_DOCS else None
|
||||||
|
|
||||||
|
# ===============================================
|
||||||
|
# Database Configuration
|
||||||
|
|
||||||
|
DB_ENGINE: str = "sqlite" # Options: 'sqlite', 'postgres'
|
||||||
|
DB_PROVIDER: AbstractDBProvider = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def DB_URL(self) -> str:
|
||||||
|
return self.DB_PROVIDER.db_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def DB_URL_PUBLIC(self) -> str:
|
||||||
|
return self.DB_PROVIDER.db_url_public
|
||||||
|
|
||||||
|
DEFAULT_GROUP: str = "Home"
|
||||||
|
DEFAULT_EMAIL: str = "changeme@email.com"
|
||||||
|
DEFAULT_PASSWORD: str = "MyPassword"
|
||||||
|
|
||||||
|
# ===============================================
|
||||||
|
# Email Configuration
|
||||||
|
|
||||||
|
SMTP_HOST: Optional[str]
|
||||||
|
SMTP_PORT: Optional[str] = "587"
|
||||||
|
SMTP_FROM_NAME: Optional[str] = "Mealie"
|
||||||
|
SMTP_TLS: Optional[bool] = True
|
||||||
|
SMTP_FROM_EMAIL: Optional[str]
|
||||||
|
SMTP_USER: Optional[str]
|
||||||
|
SMTP_PASSWORD: Optional[str]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def SMTP_ENABLE(self) -> bool:
|
||||||
|
"""Validates all SMTP variables are set"""
|
||||||
|
required = {
|
||||||
|
self.SMTP_HOST,
|
||||||
|
self.SMTP_PORT,
|
||||||
|
self.SMTP_FROM_NAME,
|
||||||
|
self.SMTP_TLS,
|
||||||
|
self.SMTP_FROM_EMAIL,
|
||||||
|
self.SMTP_USER,
|
||||||
|
self.SMTP_PASSWORD,
|
||||||
|
}
|
||||||
|
|
||||||
|
return "" not in required and None not in required
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
arbitrary_types_allowed = True
|
||||||
|
|
||||||
|
|
||||||
|
def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, env_encoding="utf-8") -> AppSettings:
|
||||||
|
"""
|
||||||
|
app_settings_constructor is a factory function that returns an AppSettings object. It is used to inject the
|
||||||
|
required dependencies into the AppSettings object and nested child objects. AppSettings should not be substantiated
|
||||||
|
directly, but rather through this factory function.
|
||||||
|
"""
|
||||||
|
app_settings = AppSettings(
|
||||||
|
_env_file=env_file,
|
||||||
|
_env_file_encoding=env_encoding,
|
||||||
|
**{"SECRET": determine_secrets(data_dir, production)},
|
||||||
|
)
|
||||||
|
|
||||||
|
app_settings.DB_PROVIDER = db_provider_factory(
|
||||||
|
app_settings.DB_ENGINE or "sqlite",
|
||||||
|
data_dir,
|
||||||
|
env_file=env_file,
|
||||||
|
env_encoding=env_encoding,
|
||||||
|
)
|
||||||
|
|
||||||
|
return app_settings
|
10
mealie/core/settings/static.py
Normal file
10
mealie/core/settings/static.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
APP_VERSION = "v1.0.0b"
|
||||||
|
DB_VERSION = "v1.0.0b"
|
||||||
|
|
||||||
|
CWD = Path(__file__).parent
|
||||||
|
BASE_DIR = CWD.parent.parent.parent
|
||||||
|
|
||||||
|
PRODUCTION = os.getenv("PRODUCTION", "True").lower() in ["true", "1"]
|
@ -16,6 +16,7 @@ from mealie.db.models.recipe.tag import Tag
|
|||||||
from mealie.db.models.settings import SiteSettings
|
from mealie.db.models.settings import SiteSettings
|
||||||
from mealie.db.models.sign_up import SignUp
|
from mealie.db.models.sign_up import SignUp
|
||||||
from mealie.db.models.users import LongLiveToken, User
|
from mealie.db.models.users import LongLiveToken, User
|
||||||
|
from mealie.db.models.users.password_reset import PasswordResetModel
|
||||||
from mealie.schema.admin import SiteSettings as SiteSettingsSchema
|
from mealie.schema.admin import SiteSettings as SiteSettingsSchema
|
||||||
from mealie.schema.cookbook.cookbook import ReadCookBook
|
from mealie.schema.cookbook.cookbook import ReadCookBook
|
||||||
from mealie.schema.events import Event as EventSchema
|
from mealie.schema.events import Event as EventSchema
|
||||||
@ -27,6 +28,7 @@ from mealie.schema.meal_plan.new_meal import ReadPlanEntry
|
|||||||
from mealie.schema.recipe import CommentOut, Recipe, RecipeCategoryResponse, RecipeTagResponse
|
from mealie.schema.recipe import CommentOut, Recipe, RecipeCategoryResponse, RecipeTagResponse
|
||||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
|
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
|
||||||
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser, SignUpOut
|
from mealie.schema.user import GroupInDB, LongLiveTokenInDB, PrivateUser, SignUpOut
|
||||||
|
from mealie.schema.user.user_passwords import PrivatePasswordResetToken
|
||||||
|
|
||||||
from ._access_model import AccessModel
|
from ._access_model import AccessModel
|
||||||
from .group_access_model import GroupDataAccessModel
|
from .group_access_model import GroupDataAccessModel
|
||||||
@ -117,6 +119,10 @@ class Database:
|
|||||||
def api_tokens(self) -> AccessModel:
|
def api_tokens(self) -> AccessModel:
|
||||||
return AccessModel(self.session, pk_id, LongLiveToken, LongLiveTokenInDB)
|
return AccessModel(self.session, pk_id, LongLiveToken, LongLiveTokenInDB)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def tokens_pw_reset(self) -> AccessModel[PrivatePasswordResetToken, PasswordResetModel]:
|
||||||
|
return AccessModel(self.session, pk_token, PasswordResetModel, PrivatePasswordResetToken)
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# Group Items
|
# Group Items
|
||||||
|
|
||||||
@ -126,7 +132,7 @@ class Database:
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def group_invite_tokens(self) -> AccessModel:
|
def group_invite_tokens(self) -> AccessModel:
|
||||||
return AccessModel(self.session, "token", GroupInviteToken, ReadInviteToken)
|
return AccessModel(self.session, pk_token, GroupInviteToken, ReadInviteToken)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def group_preferences(self) -> AccessModel:
|
def group_preferences(self) -> AccessModel:
|
||||||
|
@ -5,9 +5,9 @@ from ._access_model import AccessModel
|
|||||||
|
|
||||||
|
|
||||||
class UserDataAccessModel(AccessModel[PrivateUser, User]):
|
class UserDataAccessModel(AccessModel[PrivateUser, User]):
|
||||||
def update_password(self, session, id, password: str):
|
def update_password(self, id, password: str):
|
||||||
entry = self._query_one(match_value=id)
|
entry = self._query_one(match_value=id)
|
||||||
entry.update_password(password)
|
entry.update_password(password)
|
||||||
session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
return self.schema.from_orm(entry)
|
return self.schema.from_orm(entry)
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
from mealie.core import root_logger
|
from mealie.core import root_logger
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import get_app_settings
|
||||||
from mealie.core.security import hash_password
|
from mealie.core.security import hash_password
|
||||||
from mealie.db.data_access_layer.access_model_factory import Database
|
from mealie.db.data_access_layer.access_model_factory import Database
|
||||||
|
|
||||||
logger = root_logger.get_logger("init_users")
|
logger = root_logger.get_logger("init_users")
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
|
||||||
def dev_users() -> list[dict]:
|
def dev_users() -> list[dict]:
|
||||||
|
@ -2,7 +2,9 @@ import sqlalchemy as sa
|
|||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import get_app_settings
|
||||||
|
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
|
||||||
def sql_global_init(db_url: str):
|
def sql_global_init(db_url: str):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from mealie.core import root_logger
|
from mealie.core import root_logger
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import get_app_settings
|
||||||
from mealie.db.data_access_layer.access_model_factory import Database
|
from mealie.db.data_access_layer.access_model_factory import Database
|
||||||
from mealie.db.data_initialization.init_units_foods import default_recipe_unit_init
|
from mealie.db.data_initialization.init_units_foods import default_recipe_unit_init
|
||||||
from mealie.db.data_initialization.init_users import default_user_init
|
from mealie.db.data_initialization.init_users import default_user_init
|
||||||
@ -13,6 +13,8 @@ from mealie.services.group_services.group_utils import create_new_group
|
|||||||
|
|
||||||
logger = root_logger.get_logger("init_db")
|
logger = root_logger.get_logger("init_db")
|
||||||
|
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
|
||||||
def create_all_models():
|
def create_all_models():
|
||||||
import mealie.db.models._all_models # noqa: F401
|
import mealie.db.models._all_models # noqa: F401
|
||||||
|
@ -2,7 +2,7 @@ import sqlalchemy as sa
|
|||||||
import sqlalchemy.orm as orm
|
import sqlalchemy.orm as orm
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import get_app_settings
|
||||||
from mealie.db.models.group.invite_tokens import GroupInviteToken
|
from mealie.db.models.group.invite_tokens import GroupInviteToken
|
||||||
|
|
||||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||||
@ -13,6 +13,8 @@ from .cookbook import CookBook
|
|||||||
from .mealplan import GroupMealPlan
|
from .mealplan import GroupMealPlan
|
||||||
from .preferences import GroupPreferencesModel
|
from .preferences import GroupPreferencesModel
|
||||||
|
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
|
||||||
class Group(SqlAlchemyBase, BaseMixins):
|
class Group(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "groups"
|
__tablename__ = "groups"
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
|
from .password_reset import *
|
||||||
from .user_to_favorite import *
|
from .user_to_favorite import *
|
||||||
from .users import *
|
from .users import *
|
||||||
|
15
mealie/db/models/users/password_reset.py
Normal file
15
mealie/db/models/users/password_reset.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from sqlalchemy import Column, ForeignKey, Integer, String, orm
|
||||||
|
|
||||||
|
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetModel(SqlAlchemyBase, BaseMixins):
|
||||||
|
__tablename__ = "password_reset_tokens"
|
||||||
|
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
user = orm.relationship("User", back_populates="password_reset_tokens", uselist=False)
|
||||||
|
token = Column(String(64), unique=True, nullable=False)
|
||||||
|
|
||||||
|
def __init__(self, user_id, token, **_):
|
||||||
|
self.user_id = user_id
|
||||||
|
self.token = token
|
@ -1,11 +1,13 @@
|
|||||||
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
|
||||||
|
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import get_app_settings
|
||||||
|
|
||||||
from .._model_base import BaseMixins, SqlAlchemyBase
|
from .._model_base import BaseMixins, SqlAlchemyBase
|
||||||
from ..group import Group
|
from ..group import Group
|
||||||
from .user_to_favorite import users_to_favorites
|
from .user_to_favorite import users_to_favorites
|
||||||
|
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
|
||||||
class LongLiveToken(SqlAlchemyBase, BaseMixins):
|
class LongLiveToken(SqlAlchemyBase, BaseMixins):
|
||||||
__tablename__ = "long_live_tokens"
|
__tablename__ = "long_live_tokens"
|
||||||
@ -48,6 +50,10 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||||||
"RecipeComment", back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
|
"RecipeComment", back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
password_reset_tokens = orm.relationship(
|
||||||
|
"PasswordResetModel", back_populates="user", cascade="all, delete, delete-orphan", single_parent=True
|
||||||
|
)
|
||||||
|
|
||||||
owned_recipes_id = Column(Integer, ForeignKey("recipes.id"))
|
owned_recipes_id = Column(Integer, ForeignKey("recipes.id"))
|
||||||
owned_recipes = orm.relationship("RecipeModel", single_parent=True, foreign_keys=[owned_recipes_id])
|
owned_recipes = orm.relationship("RecipeModel", single_parent=True, foreign_keys=[owned_recipes_id])
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import APP_VERSION, get_settings
|
from mealie.core.config import get_app_settings
|
||||||
|
from mealie.core.settings.static import APP_VERSION
|
||||||
from mealie.db.database import get_database
|
from mealie.db.database import get_database
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.schema.admin.about import AdminAboutInfo, AppStatistics, CheckAppConfig
|
from mealie.schema.admin.about import AdminAboutInfo, AppStatistics, CheckAppConfig
|
||||||
@ -12,7 +13,7 @@ router = APIRouter(prefix="/about")
|
|||||||
@router.get("", response_model=AdminAboutInfo)
|
@router.get("", response_model=AdminAboutInfo)
|
||||||
async def get_app_info():
|
async def get_app_info():
|
||||||
""" Get general application information """
|
""" Get general application information """
|
||||||
settings = get_settings()
|
settings = get_app_settings()
|
||||||
|
|
||||||
return AdminAboutInfo(
|
return AdminAboutInfo(
|
||||||
production=settings.PRODUCTION,
|
production=settings.PRODUCTION,
|
||||||
@ -40,7 +41,7 @@ async def get_app_statistics(session: Session = Depends(generate_session)):
|
|||||||
|
|
||||||
@router.get("/check", response_model=CheckAppConfig)
|
@router.get("/check", response_model=CheckAppConfig)
|
||||||
async def check_app_config():
|
async def check_app_config():
|
||||||
settings = get_settings()
|
settings = get_app_settings()
|
||||||
|
|
||||||
url_set = settings.BASE_URL != "http://localhost:8080"
|
url_set = settings.BASE_URL != "http://localhost:8080"
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
|
|
||||||
from mealie.core.config import get_settings
|
from mealie.core.config import get_app_settings
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.services.email import EmailService
|
from mealie.services.email import EmailService
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ class EmailTest(CamelModel):
|
|||||||
@router.get("", response_model=EmailReady)
|
@router.get("", response_model=EmailReady)
|
||||||
async def check_email_config():
|
async def check_email_config():
|
||||||
""" Get general application information """
|
""" Get general application information """
|
||||||
settings = get_settings()
|
settings = get_app_settings()
|
||||||
|
|
||||||
return EmailReady(ready=settings.SMTP_ENABLE)
|
return EmailReady(ready=settings.SMTP_ENABLE)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from mealie.core.config import APP_VERSION, get_settings
|
from mealie.core.config import APP_VERSION, get_app_settings
|
||||||
from mealie.schema.admin.about import AppInfo
|
from mealie.schema.admin.about import AppInfo
|
||||||
|
|
||||||
router = APIRouter(prefix="/about")
|
router = APIRouter(prefix="/about")
|
||||||
@ -9,7 +9,7 @@ router = APIRouter(prefix="/about")
|
|||||||
@router.get("", response_model=AppInfo)
|
@router.get("", response_model=AppInfo)
|
||||||
async def get_app_info():
|
async def get_app_info():
|
||||||
""" Get general application information """
|
""" Get general application information """
|
||||||
settings = get_settings()
|
settings = get_app_settings()
|
||||||
|
|
||||||
return AppInfo(
|
return AppInfo(
|
||||||
version=APP_VERSION,
|
version=APP_VERSION,
|
||||||
|
@ -5,7 +5,9 @@ from pathlib import Path
|
|||||||
from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, status
|
from fastapi import BackgroundTasks, Depends, File, HTTPException, UploadFile, status
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import app_dirs
|
from mealie.core.config import get_app_dirs
|
||||||
|
|
||||||
|
app_dirs = get_app_dirs()
|
||||||
from mealie.core.dependencies import get_current_user
|
from mealie.core.dependencies import get_current_user
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.core.security import create_file_token
|
from mealie.core.security import create_file_token
|
||||||
|
@ -5,7 +5,9 @@ from typing import List
|
|||||||
from fastapi import Depends, File, HTTPException, UploadFile, status
|
from fastapi import Depends, File, HTTPException, UploadFile, status
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import app_dirs
|
from mealie.core.config import get_app_dirs
|
||||||
|
|
||||||
|
app_dirs = get_app_dirs()
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.routers import AdminAPIRouter
|
from mealie.routes.routers import AdminAPIRouter
|
||||||
from mealie.routes.users.crud import get_logged_in_user
|
from mealie.routes.users.crud import get_logged_in_user
|
||||||
|
@ -13,6 +13,7 @@ router.include_router(crud.user_router, prefix=user_prefix, tags=["Users: CRUD"]
|
|||||||
router.include_router(crud.admin_router, prefix=user_prefix, tags=["Users: CRUD"])
|
router.include_router(crud.admin_router, prefix=user_prefix, tags=["Users: CRUD"])
|
||||||
|
|
||||||
router.include_router(passwords.user_router, prefix=user_prefix, tags=["Users: Passwords"])
|
router.include_router(passwords.user_router, prefix=user_prefix, tags=["Users: Passwords"])
|
||||||
|
router.include_router(passwords.public_router, prefix=user_prefix, tags=["Users: Passwords"])
|
||||||
|
|
||||||
router.include_router(images.public_router, prefix=user_prefix, tags=["Users: Images"])
|
router.include_router(images.public_router, prefix=user_prefix, tags=["Users: Images"])
|
||||||
router.include_router(images.user_router, prefix=user_prefix, tags=["Users: Images"])
|
router.include_router(images.user_router, prefix=user_prefix, tags=["Users: Images"])
|
||||||
|
@ -4,7 +4,9 @@ from fastapi import Depends, File, HTTPException, UploadFile, status
|
|||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
|
|
||||||
from mealie.core.config import app_dirs
|
from mealie.core.config import get_app_dirs
|
||||||
|
|
||||||
|
app_dirs = get_app_dirs()
|
||||||
from mealie.core.dependencies import get_current_user
|
from mealie.core.dependencies import get_current_user
|
||||||
from mealie.routes.routers import UserAPIRouter
|
from mealie.routes.routers import UserAPIRouter
|
||||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
from fastapi import Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import get_app_settings
|
||||||
from mealie.core.security import hash_password
|
from mealie.core.security import hash_password
|
||||||
from mealie.db.database import get_database
|
from mealie.db.database import get_database
|
||||||
from mealie.db.db_setup import generate_session
|
from mealie.db.db_setup import generate_session
|
||||||
from mealie.routes.routers import UserAPIRouter
|
from mealie.routes.routers import UserAPIRouter
|
||||||
from mealie.schema.user import ChangePassword
|
from mealie.schema.user import ChangePassword
|
||||||
|
from mealie.schema.user.user_passwords import ForgotPassword, ResetPassword
|
||||||
from mealie.services.user_services import UserService
|
from mealie.services.user_services import UserService
|
||||||
|
from mealie.services.user_services.password_reset_service import PasswordResetService
|
||||||
|
|
||||||
user_router = UserAPIRouter(prefix="")
|
user_router = UserAPIRouter(prefix="")
|
||||||
|
public_router = APIRouter(prefix="")
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
|
||||||
@user_router.put("/{id}/reset-password")
|
@user_router.put("/{id}/reset-password")
|
||||||
@ -25,3 +29,17 @@ def update_password(password_change: ChangePassword, user_service: UserService =
|
|||||||
""" Resets the User Password"""
|
""" Resets the User Password"""
|
||||||
|
|
||||||
return user_service.change_password(password_change)
|
return user_service.change_password(password_change)
|
||||||
|
|
||||||
|
|
||||||
|
@public_router.post("/forgot-password")
|
||||||
|
def forgot_password(email: ForgotPassword, session: Session = Depends(generate_session)):
|
||||||
|
""" Sends an email with a reset link to the user"""
|
||||||
|
f_service = PasswordResetService(session)
|
||||||
|
return f_service.send_reset_email(email.email)
|
||||||
|
|
||||||
|
|
||||||
|
@public_router.post("/reset-password")
|
||||||
|
def reset_password(reset_password: ResetPassword, session: Session = Depends(generate_session)):
|
||||||
|
""" Resets the user password"""
|
||||||
|
f_service = PasswordResetService(session)
|
||||||
|
return f_service.reset_password(reset_password.token, reset_password.password)
|
||||||
|
@ -7,7 +7,7 @@ from pydantic import BaseModel, Field, validator
|
|||||||
from pydantic.utils import GetterDict
|
from pydantic.utils import GetterDict
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
||||||
from mealie.core.config import app_dirs
|
from mealie.core.config import get_app_dirs
|
||||||
from mealie.db.models.recipe.recipe import RecipeModel
|
from mealie.db.models.recipe.recipe import RecipeModel
|
||||||
|
|
||||||
from .recipe_asset import RecipeAsset
|
from .recipe_asset import RecipeAsset
|
||||||
@ -18,6 +18,8 @@ from .recipe_nutrition import Nutrition
|
|||||||
from .recipe_settings import RecipeSettings
|
from .recipe_settings import RecipeSettings
|
||||||
from .recipe_step import RecipeStep
|
from .recipe_step import RecipeStep
|
||||||
|
|
||||||
|
app_dirs = get_app_dirs()
|
||||||
|
|
||||||
|
|
||||||
class CreateRecipeByURL(BaseModel):
|
class CreateRecipeByURL(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
from fastapi_camelcase import CamelModel
|
from fastapi_camelcase import CamelModel
|
||||||
|
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import get_app_settings
|
||||||
|
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
|
||||||
class RecipeSettings(CamelModel):
|
class RecipeSettings(CamelModel):
|
||||||
public: bool = settings.RECIPE_PUBLIC
|
public: bool = False
|
||||||
show_nutrition: bool = settings.RECIPE_SHOW_NUTRITION
|
show_nutrition: bool = False
|
||||||
show_assets: bool = settings.RECIPE_SHOW_ASSETS
|
show_assets: bool = False
|
||||||
landscape_view: bool = settings.RECIPE_LANDSCAPE_VIEW
|
landscape_view: bool = False
|
||||||
disable_comments: bool = settings.RECIPE_DISABLE_COMMENTS
|
disable_comments: bool = True
|
||||||
disable_amount: bool = settings.RECIPE_DISABLE_AMOUNT
|
disable_amount: bool = True
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
@ -5,7 +5,7 @@ from fastapi_camelcase import CamelModel
|
|||||||
from pydantic.types import constr
|
from pydantic.types import constr
|
||||||
from pydantic.utils import GetterDict
|
from pydantic.utils import GetterDict
|
||||||
|
|
||||||
from mealie.core.config import settings
|
from mealie.core.config import get_app_settings
|
||||||
from mealie.db.models.users import User
|
from mealie.db.models.users import User
|
||||||
from mealie.schema.group.group_preferences import ReadGroupPreferences
|
from mealie.schema.group.group_preferences import ReadGroupPreferences
|
||||||
from mealie.schema.recipe import RecipeSummary
|
from mealie.schema.recipe import RecipeSummary
|
||||||
@ -13,6 +13,8 @@ from mealie.schema.recipe import RecipeSummary
|
|||||||
from ..meal_plan import ShoppingListOut
|
from ..meal_plan import ShoppingListOut
|
||||||
from ..recipe import CategoryBase
|
from ..recipe import CategoryBase
|
||||||
|
|
||||||
|
settings = get_app_settings()
|
||||||
|
|
||||||
|
|
||||||
class LoingLiveTokenIn(CamelModel):
|
class LoingLiveTokenIn(CamelModel):
|
||||||
name: str
|
name: str
|
||||||
|
29
mealie/schema/user/user_passwords.py
Normal file
29
mealie/schema/user/user_passwords.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from fastapi_camelcase import CamelModel
|
||||||
|
|
||||||
|
from .user import PrivateUser
|
||||||
|
|
||||||
|
|
||||||
|
class ForgotPassword(CamelModel):
|
||||||
|
email: str
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateResetToken(CamelModel):
|
||||||
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPassword(ValidateResetToken):
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
passwordConfirm: str
|
||||||
|
|
||||||
|
|
||||||
|
class SavePasswordResetToken(CamelModel):
|
||||||
|
user_id: int
|
||||||
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
class PrivatePasswordResetToken(SavePasswordResetToken):
|
||||||
|
user: PrivateUser
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
@ -5,7 +5,7 @@ from fastapi import BackgroundTasks, Depends, HTTPException, status
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import get_app_dirs, get_settings
|
from mealie.core.config import get_app_dirs, get_app_settings
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.db.database import get_database
|
from mealie.db.database import get_database
|
||||||
from mealie.db.db_setup import SessionLocal
|
from mealie.db.db_setup import SessionLocal
|
||||||
@ -63,7 +63,7 @@ class BaseHttpService(Generic[T, D], ABC):
|
|||||||
# Static Globals Dependency Injection
|
# Static Globals Dependency Injection
|
||||||
self.db = get_database(session)
|
self.db = get_database(session)
|
||||||
self.app_dirs = get_app_dirs()
|
self.app_dirs = get_app_dirs()
|
||||||
self.settings = get_settings()
|
self.settings = get_app_settings()
|
||||||
|
|
||||||
def _existing_factory(dependency: Type[CLS_DEP]) -> classmethod:
|
def _existing_factory(dependency: Type[CLS_DEP]) -> classmethod:
|
||||||
def cls_method(cls, item_id: T, deps: CLS_DEP = Depends(dependency)):
|
def cls_method(cls, item_id: T, deps: CLS_DEP = Depends(dependency)):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from mealie.core.config import get_app_dirs, get_settings
|
from mealie.core.config import get_app_dirs, get_app_settings
|
||||||
|
|
||||||
|
|
||||||
class BaseService:
|
class BaseService:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.app_dirs = get_app_dirs()
|
self.app_dirs = get_app_dirs()
|
||||||
self.settings = get_settings()
|
self.settings = get_app_settings()
|
||||||
|
@ -9,10 +9,10 @@ from pathvalidate import sanitize_filename
|
|||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
|
|
||||||
from mealie.core import root_logger
|
from mealie.core import root_logger
|
||||||
from mealie.core.config import app_dirs
|
from mealie.core.config import get_app_dirs
|
||||||
|
|
||||||
|
app_dirs = get_app_dirs()
|
||||||
from mealie.db.database import get_database
|
from mealie.db.database import get_database
|
||||||
from mealie.db.db_setup import create_session
|
|
||||||
from mealie.services.events import create_backup_event
|
|
||||||
|
|
||||||
logger = root_logger.get_logger()
|
logger = root_logger.get_logger()
|
||||||
|
|
||||||
@ -141,15 +141,3 @@ def backup_all(
|
|||||||
db_export.export_items(all_notifications, "notifications")
|
db_export.export_items(all_notifications, "notifications")
|
||||||
|
|
||||||
return db_export.finish_export()
|
return db_export.finish_export()
|
||||||
|
|
||||||
|
|
||||||
def auto_backup_job():
|
|
||||||
for backup in app_dirs.BACKUP_DIR.glob("Auto*.zip"):
|
|
||||||
backup.unlink()
|
|
||||||
|
|
||||||
templates = [template for template in app_dirs.TEMPLATE_DIR.iterdir()]
|
|
||||||
session = create_session()
|
|
||||||
backup_all(session=session, tag="Auto", templates=templates)
|
|
||||||
logger.info("Auto Backup Called")
|
|
||||||
create_backup_event("Automated Backup", "Automated backup created", session)
|
|
||||||
session.close()
|
|
||||||
|
@ -7,7 +7,9 @@ from typing import Callable
|
|||||||
from pydantic.main import BaseModel
|
from pydantic.main import BaseModel
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import app_dirs
|
from mealie.core.config import get_app_dirs
|
||||||
|
|
||||||
|
app_dirs = get_app_dirs()
|
||||||
from mealie.db.database import get_database
|
from mealie.db.database import get_database
|
||||||
from mealie.schema.admin import (
|
from mealie.schema.admin import (
|
||||||
CommentImport,
|
CommentImport,
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
|
|
||||||
from mealie.core.dependencies.grouped import UserDeps
|
from mealie.core.dependencies.grouped import UserDeps
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
|
from mealie.core.security import url_safe_token
|
||||||
from mealie.schema.group.group_permissions import SetPermissions
|
from mealie.schema.group.group_permissions import SetPermissions
|
||||||
from mealie.schema.group.group_preferences import UpdateGroupPreferences
|
from mealie.schema.group.group_preferences import UpdateGroupPreferences
|
||||||
from mealie.schema.group.invite_token import EmailInitationResponse, EmailInvitation, ReadInviteToken, SaveInviteToken
|
from mealie.schema.group.invite_token import EmailInitationResponse, EmailInvitation, ReadInviteToken, SaveInviteToken
|
||||||
@ -86,7 +85,7 @@ class GroupSelfService(UserHttpService[int, str]):
|
|||||||
if not self.user.can_invite:
|
if not self.user.can_invite:
|
||||||
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="User is not allowed to create invite tokens")
|
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="User is not allowed to create invite tokens")
|
||||||
|
|
||||||
token = SaveInviteToken(uses_left=uses, group_id=self.group_id, token=uuid4().hex)
|
token = SaveInviteToken(uses_left=uses, group_id=self.group_id, token=url_safe_token())
|
||||||
return self.db.group_invite_tokens.create(token)
|
return self.db.group_invite_tokens.create(token)
|
||||||
|
|
||||||
def get_invite_tokens(self) -> list[ReadInviteToken]:
|
def get_invite_tokens(self) -> list[ReadInviteToken]:
|
||||||
|
@ -5,10 +5,11 @@ from pathlib import Path
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from mealie.core import root_logger
|
from mealie.core import root_logger
|
||||||
from mealie.core.config import app_dirs
|
from mealie.core.config import get_app_dirs
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
|
|
||||||
logger = root_logger.get_logger()
|
logger = root_logger.get_logger()
|
||||||
|
app_dirs = get_app_dirs()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -3,7 +3,9 @@ from typing import Optional
|
|||||||
|
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from mealie.core.config import app_dirs
|
from mealie.core.config import get_app_dirs
|
||||||
|
|
||||||
|
app_dirs = get_app_dirs()
|
||||||
from mealie.schema.admin import MigrationImport
|
from mealie.schema.admin import MigrationImport
|
||||||
from mealie.schema.user.user import PrivateUser
|
from mealie.schema.user.user import PrivateUser
|
||||||
from mealie.services.migrations import helpers
|
from mealie.services.migrations import helpers
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
from .scheduler_registry import *
|
||||||
|
from .scheduler_service import *
|
@ -1,7 +0,0 @@
|
|||||||
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
|
||||||
|
|
||||||
from mealie.core.config import app_dirs, settings
|
|
||||||
|
|
||||||
app_dirs.DATA_DIR.joinpath("scheduler.db").unlink(missing_ok=True)
|
|
||||||
scheduler = BackgroundScheduler(jobstores={"default": SQLAlchemyJobStore(settings.SCHEDULER_DATABASE)})
|
|
30
mealie/services/scheduler/scheduled_func.py
Normal file
30
mealie/services/scheduler/scheduled_func.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Callable, Tuple
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Cron:
|
||||||
|
hours: int
|
||||||
|
minutes: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, time_str: str) -> Cron:
|
||||||
|
time = time_str.split(":")
|
||||||
|
return Cron(hours=int(time[0]), minutes=int(time[1]))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ScheduledFunc(BaseModel):
|
||||||
|
id: Tuple[str, int]
|
||||||
|
name: str
|
||||||
|
hour: int
|
||||||
|
minutes: int
|
||||||
|
callback: Callable
|
||||||
|
|
||||||
|
max_instances: int = 1
|
||||||
|
replace_existing: bool = True
|
||||||
|
args: list = []
|
@ -1,124 +0,0 @@
|
|||||||
import datetime
|
|
||||||
|
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
|
||||||
|
|
||||||
from mealie.core import root_logger
|
|
||||||
from mealie.db.database import get_database
|
|
||||||
from mealie.db.db_setup import create_session
|
|
||||||
from mealie.db.models.event import Event
|
|
||||||
from mealie.schema.user import GroupInDB
|
|
||||||
from mealie.services.backups.exports import auto_backup_job
|
|
||||||
from mealie.services.scheduler.global_scheduler import scheduler
|
|
||||||
from mealie.services.scheduler.scheduler_utils import Cron, cron_parser
|
|
||||||
from mealie.utils.post_webhooks import post_webhooks
|
|
||||||
|
|
||||||
logger = root_logger.get_logger()
|
|
||||||
|
|
||||||
# TODO Fix Scheduler
|
|
||||||
|
|
||||||
|
|
||||||
@scheduler.scheduled_job(trigger="interval", minutes=1440)
|
|
||||||
def purge_events_database():
|
|
||||||
"""
|
|
||||||
Ran daily. Purges all events after 100
|
|
||||||
"""
|
|
||||||
logger.info("Purging Events in Database")
|
|
||||||
expiration_days = 7
|
|
||||||
limit = datetime.datetime.now() - datetime.timedelta(days=expiration_days)
|
|
||||||
session = create_session()
|
|
||||||
session.query(Event).filter(Event.time_stamp <= limit).delete()
|
|
||||||
session.commit()
|
|
||||||
session.close()
|
|
||||||
logger.info("Events Purges")
|
|
||||||
|
|
||||||
|
|
||||||
@scheduler.scheduled_job(trigger="interval", minutes=30)
|
|
||||||
def update_webhook_schedule():
|
|
||||||
"""
|
|
||||||
A scheduled background job that runs every 30 minutes to
|
|
||||||
poll the database for changes and reschedule the webhook time
|
|
||||||
"""
|
|
||||||
session = create_session()
|
|
||||||
db = get_database(session)
|
|
||||||
all_groups: list[GroupInDB] = db.groups.get_all()
|
|
||||||
|
|
||||||
for group in all_groups:
|
|
||||||
|
|
||||||
time = cron_parser(group.webhook_time)
|
|
||||||
job = JOB_STORE.get(group.name)
|
|
||||||
|
|
||||||
if not job:
|
|
||||||
logger.error(f"No job found for group: {group.name}")
|
|
||||||
logger.info(f"Creating scheduled task for {group.name}")
|
|
||||||
JOB_STORE.update(add_group_to_schedule(scheduler, group))
|
|
||||||
continue
|
|
||||||
|
|
||||||
scheduler.reschedule_job(
|
|
||||||
job.scheduled_task.id,
|
|
||||||
trigger="cron",
|
|
||||||
hour=time.hours,
|
|
||||||
minute=time.minutes,
|
|
||||||
)
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
logger.info(scheduler.print_jobs())
|
|
||||||
|
|
||||||
|
|
||||||
class ScheduledFunction:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
scheduler: BackgroundScheduler,
|
|
||||||
function,
|
|
||||||
cron: Cron,
|
|
||||||
name: str,
|
|
||||||
args: list = None,
|
|
||||||
) -> None:
|
|
||||||
self.scheduled_task = scheduler.add_job(
|
|
||||||
function,
|
|
||||||
trigger="cron",
|
|
||||||
name=name,
|
|
||||||
hour=cron.hours,
|
|
||||||
minute=cron.minutes,
|
|
||||||
max_instances=1,
|
|
||||||
replace_existing=True,
|
|
||||||
args=args,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def add_group_to_schedule(scheduler, group: GroupInDB):
|
|
||||||
cron = cron_parser(group.webhook_time)
|
|
||||||
|
|
||||||
return {
|
|
||||||
group.name: ScheduledFunction(
|
|
||||||
scheduler,
|
|
||||||
post_webhooks,
|
|
||||||
cron=cron,
|
|
||||||
name=group.name,
|
|
||||||
args=[group.id],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def init_webhook_schedule(scheduler, job_store: dict):
|
|
||||||
session = create_session()
|
|
||||||
db = get_database(session)
|
|
||||||
all_groups: list[GroupInDB] = db.groups.get_all()
|
|
||||||
|
|
||||||
for group in all_groups:
|
|
||||||
job_store.update(add_group_to_schedule(scheduler, group))
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return job_store
|
|
||||||
|
|
||||||
|
|
||||||
logger.info("----INIT SCHEDULE OBJECT-----")
|
|
||||||
|
|
||||||
JOB_STORE = {
|
|
||||||
"backup_job": ScheduledFunction(scheduler, auto_backup_job, Cron(hours=00, minutes=00), "backups"),
|
|
||||||
}
|
|
||||||
|
|
||||||
JOB_STORE = init_webhook_schedule(scheduler=scheduler, job_store=JOB_STORE)
|
|
||||||
|
|
||||||
logger.info(scheduler.print_jobs())
|
|
||||||
scheduler.start()
|
|
43
mealie/services/scheduler/scheduler_registry.py
Normal file
43
mealie/services/scheduler/scheduler_registry.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from mealie.core import root_logger
|
||||||
|
|
||||||
|
logger = root_logger.get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class SchedulerRegistry:
|
||||||
|
"""
|
||||||
|
A container class for registring and removing callbacks for the scheduler.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_daily: list[Callable] = []
|
||||||
|
_hourly: list[Callable] = []
|
||||||
|
_minutely: list[Callable] = []
|
||||||
|
|
||||||
|
def _register(name: str, callbacks: list[Callable], callback: Callable):
|
||||||
|
for cb in callback:
|
||||||
|
logger.info(f"Registering {name} callback: {cb.__name__}")
|
||||||
|
callbacks.append(cb)
|
||||||
|
|
||||||
|
def register_daily(*callbacks: Callable):
|
||||||
|
SchedulerRegistry._register("daily", SchedulerRegistry._daily, callbacks)
|
||||||
|
|
||||||
|
def remove_daily(callback: Callable):
|
||||||
|
logger.info(f"Removing daily callback: {callback.__name__}")
|
||||||
|
SchedulerRegistry._daily.remove(callback)
|
||||||
|
|
||||||
|
def register_hourly(*callbacks: Callable):
|
||||||
|
SchedulerRegistry._register("daily", SchedulerRegistry._hourly, callbacks)
|
||||||
|
|
||||||
|
def remove_hourly(callback: Callable):
|
||||||
|
logger.info(f"Removing hourly callback: {callback.__name__}")
|
||||||
|
SchedulerRegistry._hourly.remove(callback)
|
||||||
|
|
||||||
|
def register_minutely(*callbacks: Callable):
|
||||||
|
SchedulerRegistry._register("minutely", SchedulerRegistry._minutely, callbacks)
|
||||||
|
|
||||||
|
def remove_minutely(callback: Callable):
|
||||||
|
logger.info(f"Removing minutely callback: {callback.__name__}")
|
||||||
|
SchedulerRegistry._minutely.remove(callback)
|
104
mealie/services/scheduler/scheduler_service.py
Normal file
104
mealie/services/scheduler/scheduler_service.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
||||||
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
|
||||||
|
from mealie.core import root_logger
|
||||||
|
from mealie.core.config import get_app_dirs
|
||||||
|
|
||||||
|
from .scheduled_func import ScheduledFunc
|
||||||
|
from .scheduler_registry import SchedulerRegistry
|
||||||
|
|
||||||
|
logger = root_logger.get_logger()
|
||||||
|
|
||||||
|
CWD = Path(__file__).parent
|
||||||
|
|
||||||
|
app_dirs = get_app_dirs()
|
||||||
|
TEMP_DATA = app_dirs.DATA_DIR / ".temp"
|
||||||
|
SCHEDULER_DB = TEMP_DATA / "scheduler.db"
|
||||||
|
SCHEDULER_DATABASE = f"sqlite:///{SCHEDULER_DB}"
|
||||||
|
|
||||||
|
MINUTES_DAY = 1440
|
||||||
|
MINUTES_15 = 15
|
||||||
|
MINUTES_HOUR = 60
|
||||||
|
|
||||||
|
|
||||||
|
class SchedulerService:
|
||||||
|
"""
|
||||||
|
SchedulerService is a wrapper class around the APScheduler library. It is resonpseible for interacting with the scheduler
|
||||||
|
and scheduling events. This includes the interval events that are registered in the SchedulerRegistry as well as cron events
|
||||||
|
that are used for sending webhooks. In most cases, unless the the schedule is dynamic, events should be registered with the
|
||||||
|
SchedulerRegistry. See app.py for examples.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_scheduler: BackgroundScheduler = None
|
||||||
|
# Not Sure if this is still needed?
|
||||||
|
# _job_store: dict[str, ScheduledFunc] = {}
|
||||||
|
|
||||||
|
def start():
|
||||||
|
# Preclean
|
||||||
|
SCHEDULER_DB.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
# Scaffold
|
||||||
|
TEMP_DATA.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Register Interval Jobs and Start Scheduler
|
||||||
|
SchedulerService._scheduler = BackgroundScheduler(jobstores={"default": SQLAlchemyJobStore(SCHEDULER_DATABASE)})
|
||||||
|
SchedulerService._scheduler.add_job(run_daily, "interval", minutes=MINUTES_DAY, id="Daily Interval Jobs")
|
||||||
|
SchedulerService._scheduler.add_job(run_hourly, "interval", minutes=MINUTES_HOUR, id="Hourly Interval Jobs")
|
||||||
|
SchedulerService._scheduler.add_job(run_minutely, "interval", minutes=MINUTES_15, id="Regular Interval Jobs")
|
||||||
|
SchedulerService._scheduler.start()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@property
|
||||||
|
def scheduler(cls) -> BackgroundScheduler:
|
||||||
|
return SchedulerService._scheduler
|
||||||
|
|
||||||
|
def add_cron_job(job_func: ScheduledFunc):
|
||||||
|
SchedulerService.scheduler.add_job(
|
||||||
|
job_func.callback,
|
||||||
|
trigger="cron",
|
||||||
|
name=job_func.id,
|
||||||
|
hour=job_func.hour,
|
||||||
|
minute=job_func.minutes,
|
||||||
|
max_instances=job_func.max_instances,
|
||||||
|
replace_existing=job_func.replace_existing,
|
||||||
|
args=job_func.args,
|
||||||
|
)
|
||||||
|
|
||||||
|
# SchedulerService._job_store[job_func.id] = job_func
|
||||||
|
|
||||||
|
def update_cron_job(job_func: ScheduledFunc):
|
||||||
|
SchedulerService.scheduler.reschedule_job(
|
||||||
|
job_func.id,
|
||||||
|
trigger="cron",
|
||||||
|
hour=job_func.hour,
|
||||||
|
minute=job_func.minutes,
|
||||||
|
)
|
||||||
|
|
||||||
|
# SchedulerService._job_store[job_func.id] = job_func
|
||||||
|
|
||||||
|
|
||||||
|
def _scheduled_task_wrapper(callable):
|
||||||
|
try:
|
||||||
|
callable()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in scheduled task func='{callable.__name__}': exception='{e}'")
|
||||||
|
|
||||||
|
|
||||||
|
def run_daily():
|
||||||
|
logger.info("Running daily callbacks")
|
||||||
|
for func in SchedulerRegistry._daily:
|
||||||
|
_scheduled_task_wrapper(func)
|
||||||
|
|
||||||
|
|
||||||
|
def run_hourly():
|
||||||
|
logger.info("Running hourly callbacks")
|
||||||
|
for func in SchedulerRegistry._hourly:
|
||||||
|
_scheduled_task_wrapper(func)
|
||||||
|
|
||||||
|
|
||||||
|
def run_minutely():
|
||||||
|
logger.info("Running minutely callbacks")
|
||||||
|
for func in SchedulerRegistry._minutely:
|
||||||
|
_scheduled_task_wrapper(func)
|
@ -1,8 +0,0 @@
|
|||||||
import collections
|
|
||||||
|
|
||||||
Cron = collections.namedtuple("Cron", "hours minutes")
|
|
||||||
|
|
||||||
|
|
||||||
def cron_parser(time_str: str) -> Cron:
|
|
||||||
time = time_str.split(":")
|
|
||||||
return Cron(hours=int(time[0]), minutes=int(time[1]))
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user