mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-31 10:13:32 -04:00 
			
		
		
		
	feat(backend): ✨ migrate site-settings to groups (#673)
* feat(frontend): ✨ add user registration page (WIP) * feat(backend): ✨ add user registration (WIP) * test(backend): ✅ add validator testing for registration schema * feat(backend): ✨ continued work on user sign-up * feat(backend): ✨ add signup flow and user/group settings * test(backend): ✅ user-creation tests and small refactor of existing tests * fix(backend): ✅ fix failing group tests * style: 🎨 fix lint issues Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
		| @@ -8,6 +8,8 @@ const routes = { | |||||||
|   groupsSelf: `${prefix}/groups/self`, |   groupsSelf: `${prefix}/groups/self`, | ||||||
|   categories: `${prefix}/groups/categories`, |   categories: `${prefix}/groups/categories`, | ||||||
|  |  | ||||||
|  |   preferences: `${prefix}/groups/preferences`, | ||||||
|  |  | ||||||
|   groupsId: (id: string | number) => `${prefix}/groups/${id}`, |   groupsId: (id: string | number) => `${prefix}/groups/${id}`, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -21,13 +23,34 @@ export interface CreateGroup { | |||||||
|   name: string; |   name: string; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface UpdatePreferences { | ||||||
|  |   privateGroup: boolean; | ||||||
|  |   firstDayOfWeek: number; | ||||||
|  |   recipePublic: boolean; | ||||||
|  |   recipeShowNutrition: boolean; | ||||||
|  |   recipeShowAssets: boolean; | ||||||
|  |   recipeLandscapeView: boolean; | ||||||
|  |   recipeDisableComments: boolean; | ||||||
|  |   recipeDisableAmount: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface Preferences extends UpdatePreferences { | ||||||
|  |   id: number; | ||||||
|  |   group_id: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface Group extends CreateGroup { | ||||||
|  |   id: number; | ||||||
|  |   preferences: Preferences; | ||||||
|  | } | ||||||
|  |  | ||||||
| export class GroupAPI extends BaseCRUDAPI<GroupInDB, CreateGroup> { | export class GroupAPI extends BaseCRUDAPI<GroupInDB, CreateGroup> { | ||||||
|   baseRoute = routes.groups; |   baseRoute = routes.groups; | ||||||
|   itemRoute = routes.groupsId; |   itemRoute = routes.groupsId; | ||||||
|   /** Returns the Group Data for the Current User |   /** Returns the Group Data for the Current User | ||||||
|    */ |    */ | ||||||
|   async getCurrentUserGroup() { |   async getCurrentUserGroup() { | ||||||
|     return await this.requests.get(routes.groupsSelf); |     return await this.requests.get<Group>(routes.groupsSelf); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async getCategories() { |   async getCategories() { | ||||||
| @@ -37,4 +60,12 @@ export class GroupAPI extends BaseCRUDAPI<GroupInDB, CreateGroup> { | |||||||
|   async setCategories(payload: Category[]) { |   async setCategories(payload: Category[]) { | ||||||
|     return await this.requests.put<Category[]>(routes.categories, payload); |     return await this.requests.put<Category[]>(routes.categories, payload); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   async getPreferences() { | ||||||
|  |     return await this.requests.get<Preferences>(routes.preferences); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async setPreferences(payload: UpdatePreferences) { | ||||||
|  |     return await this.requests.put<Preferences>(routes.preferences, payload); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								frontend/api/class-interfaces/user-registration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/api/class-interfaces/user-registration.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | import { BaseAPI } from "./_base"; | ||||||
|  |  | ||||||
|  | export interface RegisterPayload { | ||||||
|  |   group: string; | ||||||
|  |   groupToken: string; | ||||||
|  |   email: string; | ||||||
|  |   password: string; | ||||||
|  |   passwordConfirm: string; | ||||||
|  |   advanced: boolean; | ||||||
|  |   private: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const prefix = "/api"; | ||||||
|  |  | ||||||
|  | const routes = { | ||||||
|  |   register: `${prefix}/users/register`, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export class RegisterAPI extends BaseAPI { | ||||||
|  |   /** Returns a list of avaiable .zip files for import into Mealie. | ||||||
|  |    */ | ||||||
|  |   async register(payload: RegisterPayload) { | ||||||
|  |     return await this.requests.post<any>(routes.register, payload); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -13,6 +13,7 @@ import { UnitAPI } from "./class-interfaces/recipe-units"; | |||||||
| import { CookbookAPI } from "./class-interfaces/cookbooks"; | import { CookbookAPI } from "./class-interfaces/cookbooks"; | ||||||
| import { WebhooksAPI } from "./class-interfaces/group-webhooks"; | import { WebhooksAPI } from "./class-interfaces/group-webhooks"; | ||||||
| import { AdminAboutAPI } from "./class-interfaces/admin-about"; | import { AdminAboutAPI } from "./class-interfaces/admin-about"; | ||||||
|  | import { RegisterAPI } from "./class-interfaces/user-registration"; | ||||||
| import { ApiRequestInstance } from "~/types/api"; | import { ApiRequestInstance } from "~/types/api"; | ||||||
|  |  | ||||||
| class AdminAPI { | class AdminAPI { | ||||||
| @@ -46,6 +47,7 @@ class Api { | |||||||
|   public units: UnitAPI; |   public units: UnitAPI; | ||||||
|   public cookbooks: CookbookAPI; |   public cookbooks: CookbookAPI; | ||||||
|   public groupWebhooks: WebhooksAPI; |   public groupWebhooks: WebhooksAPI; | ||||||
|  |   public register: RegisterAPI; | ||||||
|  |  | ||||||
|   // Utils |   // Utils | ||||||
|   public upload: UploadFile; |   public upload: UploadFile; | ||||||
| @@ -67,6 +69,7 @@ class Api { | |||||||
|     this.groups = new GroupAPI(requests); |     this.groups = new GroupAPI(requests); | ||||||
|     this.cookbooks = new CookbookAPI(requests); |     this.cookbooks = new CookbookAPI(requests); | ||||||
|     this.groupWebhooks = new WebhooksAPI(requests); |     this.groupWebhooks = new WebhooksAPI(requests); | ||||||
|  |     this.register = new RegisterAPI(requests); | ||||||
|  |  | ||||||
|     // Admin |     // Admin | ||||||
|     this.events = new EventsAPI(requests); |     this.events = new EventsAPI(requests); | ||||||
|   | |||||||
| @@ -61,13 +61,7 @@ | |||||||
|                 </v-fade-transition> |                 </v-fade-transition> | ||||||
|               </v-card-title> |               </v-card-title> | ||||||
|               <v-card-text v-if="edit"> |               <v-card-text v-if="edit"> | ||||||
|                 <v-textarea |                 <v-textarea :key="'instructions' + index" v-model="value[index]['text']" auto-grow dense rows="4"> | ||||||
|                   :key="generateKey('instructions', index)" |  | ||||||
|                   v-model="value[index]['text']" |  | ||||||
|                   auto-grow |  | ||||||
|                   dense |  | ||||||
|                   rows="4" |  | ||||||
|                 > |  | ||||||
|                 </v-textarea> |                 </v-textarea> | ||||||
|               </v-card-text> |               </v-card-text> | ||||||
|               <v-expand-transition> |               <v-expand-transition> | ||||||
|   | |||||||
| @@ -134,9 +134,9 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  | import { ref } from "@nuxtjs/composition-api"; | ||||||
| import { validators } from "@/composables/use-validators"; | import { validators } from "@/composables/use-validators"; | ||||||
| import { fieldTypes } from "@/composables/forms"; | import { fieldTypes } from "@/composables/forms"; | ||||||
| import { ref } from "@nuxtjs/composition-api"; |  | ||||||
|  |  | ||||||
| const BLUR_EVENT = "blur"; | const BLUR_EVENT = "blur"; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -138,9 +138,9 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  | import { ref } from "@nuxtjs/composition-api"; | ||||||
| import { validators } from "@/composables/use-validators"; | import { validators } from "@/composables/use-validators"; | ||||||
| import { fieldTypes } from "@/composables/forms"; | import { fieldTypes } from "@/composables/forms"; | ||||||
| import { ref } from "@nuxtjs/composition-api"; |  | ||||||
|  |  | ||||||
| const BLUR_EVENT = "blur"; | const BLUR_EVENT = "blur"; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,38 @@ import { useAsyncKey } from "./use-utils"; | |||||||
| import { useApiSingleton } from "~/composables/use-api"; | import { useApiSingleton } from "~/composables/use-api"; | ||||||
| import { CreateGroup } from "~/api/class-interfaces/groups"; | import { CreateGroup } from "~/api/class-interfaces/groups"; | ||||||
|  |  | ||||||
| export const useGroup = function () { | export const useGroupSelf = function () { | ||||||
|  |   const api = useApiSingleton(); | ||||||
|  |  | ||||||
|  |   const actions = { | ||||||
|  |     get() { | ||||||
|  |       const group = useAsync(async () => { | ||||||
|  |         const { data } = await api.groups.getCurrentUserGroup(); | ||||||
|  |  | ||||||
|  |         return data; | ||||||
|  |       }, useAsyncKey()); | ||||||
|  |  | ||||||
|  |       return group; | ||||||
|  |     }, | ||||||
|  |     async updatePreferences() { | ||||||
|  |       if (!group.value) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const { data } = await api.groups.setPreferences(group.value.preferences); | ||||||
|  |  | ||||||
|  |       if (data) { | ||||||
|  |         group.value.preferences = data; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const group = actions.get(); | ||||||
|  |  | ||||||
|  |   return { actions, group }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const useGroupCategories = function () { | ||||||
|   const api = useApiSingleton(); |   const api = useApiSingleton(); | ||||||
|  |  | ||||||
|   const actions = { |   const actions = { | ||||||
| @@ -61,7 +92,6 @@ export const useGroups = function () { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async function createGroup(payload: CreateGroup) { |   async function createGroup(payload: CreateGroup) { | ||||||
|     console.log(payload); |  | ||||||
|     loading.value = true; |     loading.value = true; | ||||||
|     const { data } = await api.groups.createOne(payload); |     const { data } = await api.groups.createOne(payload); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,13 +1,10 @@ | |||||||
| <template> | <template> | ||||||
|   <v-app dark> |   <v-app dark> | ||||||
|     <!-- <TheSnackbar /> --> |  | ||||||
|  |  | ||||||
|     <AppSidebar |     <AppSidebar | ||||||
|       v-model="sidebar" |       v-model="sidebar" | ||||||
|       absolute |       absolute | ||||||
|       :top-link="topLinks" |       :top-link="topLinks" | ||||||
|       :secondary-links="$auth.user.admin ? adminLinks : null" |       :bottom-links="bottomLinks" | ||||||
|       :bottom-links="$auth.user.admin ? bottomLinks : null" |  | ||||||
|       :user="{ data: true }" |       :user="{ data: true }" | ||||||
|       :secondary-header="$t('user.admin')" |       :secondary-header="$t('user.admin')" | ||||||
|       @input="sidebar = !sidebar" |       @input="sidebar = !sidebar" | ||||||
| @@ -30,7 +27,7 @@ | |||||||
|    |    | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from "@nuxtjs/composition-api"; | import { defineComponent, 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"; | ||||||
| @@ -40,103 +37,110 @@ export default defineComponent({ | |||||||
|   middleware: "auth", |   middleware: "auth", | ||||||
|   auth: true, |   auth: true, | ||||||
|   setup() { |   setup() { | ||||||
|     return {}; |     // @ts-ignore - $globals not found in type definition | ||||||
|   }, |     const { $globals, i18n } = useContext(); | ||||||
|   data() { |  | ||||||
|  |     const sidebar = ref(null); | ||||||
|  |  | ||||||
|  |     const topLinks = [ | ||||||
|  |       { | ||||||
|  |         icon: $globals.icons.viewDashboard, | ||||||
|  |         to: "/admin/dashboard", | ||||||
|  |         title: i18n.t("sidebar.dashboard"), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         icon: $globals.icons.cog, | ||||||
|  |         to: "/admin/site-settings", | ||||||
|  |         title: i18n.t("sidebar.site-settings"), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         icon: $globals.icons.tools, | ||||||
|  |         to: "/admin/toolbox", | ||||||
|  |         title: i18n.t("sidebar.toolbox"), | ||||||
|  |         children: [ | ||||||
|  |           { | ||||||
|  |             icon: $globals.icons.bellAlert, | ||||||
|  |             to: "/admin/toolbox/notifications", | ||||||
|  |             title: i18n.t("events.notification"), | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             icon: $globals.icons.foods, | ||||||
|  |             to: "/admin/toolbox/foods", | ||||||
|  |             title: "Manage Foods", | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             icon: $globals.icons.units, | ||||||
|  |             to: "/admin/toolbox/units", | ||||||
|  |             title: "Manage Units", | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             icon: $globals.icons.tags, | ||||||
|  |             to: "/admin/toolbox/categories", | ||||||
|  |             title: i18n.t("sidebar.tags"), | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             icon: $globals.icons.tags, | ||||||
|  |             to: "/admin/toolbox/tags", | ||||||
|  |             title: i18n.t("sidebar.categories"), | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             icon: $globals.icons.broom, | ||||||
|  |             to: "/admin/toolbox/organize", | ||||||
|  |             title: i18n.t("settings.organize"), | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         icon: $globals.icons.group, | ||||||
|  |         to: "/admin/manage-users", | ||||||
|  |         title: i18n.t("sidebar.manage-users"), | ||||||
|  |         children: [ | ||||||
|  |           { | ||||||
|  |             icon: $globals.icons.user, | ||||||
|  |             to: "/admin/manage-users/all-users", | ||||||
|  |             title: i18n.t("user.users"), | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             icon: $globals.icons.group, | ||||||
|  |             to: "/admin/manage-users/all-groups", | ||||||
|  |             title: i18n.t("group.groups"), | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         icon: $globals.icons.import, | ||||||
|  |         to: "/admin/migrations", | ||||||
|  |         title: i18n.t("sidebar.migrations"), | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         icon: $globals.icons.database, | ||||||
|  |         to: "/admin/backups", | ||||||
|  |         title: i18n.t("sidebar.backups"), | ||||||
|  |       }, | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     const bottomLinks = [ | ||||||
|  |       { | ||||||
|  |         icon: $globals.icons.heart, | ||||||
|  |         title: i18n.t("about.support"), | ||||||
|  |         href: "https://github.com/sponsors/hay-kot", | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         icon: $globals.icons.information, | ||||||
|  |         title: i18n.t("about.about"), | ||||||
|  |         to: "/admin/about", | ||||||
|  |       }, | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|       sidebar: null, |       sidebar, | ||||||
|       topLinks: [ |       topLinks, | ||||||
|         { |       bottomLinks, | ||||||
|           icon: this.$globals.icons.viewDashboard, |  | ||||||
|           to: "/admin/dashboard", |  | ||||||
|           title: this.$t("sidebar.dashboard"), |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           icon: this.$globals.icons.cog, |  | ||||||
|           to: "/admin/site-settings", |  | ||||||
|           title: this.$t("sidebar.site-settings"), |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           icon: this.$globals.icons.tools, |  | ||||||
|           to: "/admin/toolbox", |  | ||||||
|           title: this.$t("sidebar.toolbox"), |  | ||||||
|           children: [ |  | ||||||
|             { |  | ||||||
|               icon: this.$globals.icons.bellAlert, |  | ||||||
|               to: "/admin/toolbox/notifications", |  | ||||||
|               title: this.$t("events.notification"), |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               icon: this.$globals.icons.foods, |  | ||||||
|               to: "/admin/toolbox/foods", |  | ||||||
|               title: "Manage Foods", |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               icon: this.$globals.icons.units, |  | ||||||
|               to: "/admin/toolbox/units", |  | ||||||
|               title: "Manage Units", |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               icon: this.$globals.icons.tags, |  | ||||||
|               to: "/admin/toolbox/categories", |  | ||||||
|               title: this.$t("sidebar.tags"), |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               icon: this.$globals.icons.tags, |  | ||||||
|               to: "/admin/toolbox/tags", |  | ||||||
|               title: this.$t("sidebar.categories"), |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               icon: this.$globals.icons.broom, |  | ||||||
|               to: "/admin/toolbox/organize", |  | ||||||
|               title: this.$t("settings.organize"), |  | ||||||
|             }, |  | ||||||
|           ], |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           icon: this.$globals.icons.group, |  | ||||||
|           to: "/admin/manage-users", |  | ||||||
|           title: this.$t("sidebar.manage-users"), |  | ||||||
|           children: [ |  | ||||||
|             { |  | ||||||
|               icon: this.$globals.icons.user, |  | ||||||
|               to: "/admin/manage-users/all-users", |  | ||||||
|               title: this.$t("user.users"), |  | ||||||
|             }, |  | ||||||
|             { |  | ||||||
|               icon: this.$globals.icons.group, |  | ||||||
|               to: "/admin/manage-users/all-groups", |  | ||||||
|               title: this.$t("group.groups"), |  | ||||||
|             }, |  | ||||||
|           ], |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           icon: this.$globals.icons.import, |  | ||||||
|           to: "/admin/migrations", |  | ||||||
|           title: this.$t("sidebar.migrations"), |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           icon: this.$globals.icons.database, |  | ||||||
|           to: "/admin/backups", |  | ||||||
|           title: this.$t("sidebar.backups"), |  | ||||||
|         }, |  | ||||||
|       ], |  | ||||||
|       bottomLinks: [ |  | ||||||
|         { |  | ||||||
|           icon: this.$globals.icons.heart, |  | ||||||
|           title: this.$t("about.support"), |  | ||||||
|           href: "https://github.com/sponsors/hay-kot", |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           icon: this.$globals.icons.information, |  | ||||||
|           title: this.$t("about.about"), |  | ||||||
|           to: "/admin/about", |  | ||||||
|         }, |  | ||||||
|       ], |  | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|        |  | ||||||
|       <style scoped> |  | ||||||
| </style>+ |  | ||||||
|  |      | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <v-app dark> |   <v-app dark> | ||||||
|     <!-- <TheSnackbar /> --> |     <TheSnackbar /> | ||||||
|  |  | ||||||
|     <AppHeader :menu="false"> </AppHeader> |     <AppHeader :menu="false"> </AppHeader> | ||||||
|     <v-main> |     <v-main> | ||||||
| @@ -17,9 +17,10 @@ | |||||||
| import { defineComponent } from "@nuxtjs/composition-api"; | import { defineComponent } from "@nuxtjs/composition-api"; | ||||||
| import AppFooter from "@/components/Layout/AppFooter.vue"; | import AppFooter from "@/components/Layout/AppFooter.vue"; | ||||||
| import AppHeader from "@/components/Layout/AppHeader.vue"; | import AppHeader from "@/components/Layout/AppHeader.vue"; | ||||||
|  | import TheSnackbar from "~/components/Layout/TheSnackbar.vue"; | ||||||
|  |  | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|   components: { AppHeader, AppFooter }, |   components: { AppHeader, AppFooter, TheSnackbar }, | ||||||
|   setup() { |   setup() { | ||||||
|     return {}; |     return {}; | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
|       :top-link="topLinks" |       :top-link="topLinks" | ||||||
|       secondary-header="Cookbooks" |       secondary-header="Cookbooks" | ||||||
|       :secondary-links="cookbookLinks || []" |       :secondary-links="cookbookLinks || []" | ||||||
|       :bottom-links="$auth.user.admin ? bottomLink : []" |       :bottom-links="isAdmin ? bottomLink : []" | ||||||
|       @input="sidebar = !sidebar" |       @input="sidebar = !sidebar" | ||||||
|     /> |     /> | ||||||
|  |  | ||||||
| @@ -37,11 +37,13 @@ import { useCookbooks } from "~/composables/use-group-cookbooks"; | |||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|   components: { AppHeader, AppSidebar, AppFloatingButton }, |   components: { AppHeader, AppSidebar, AppFloatingButton }, | ||||||
|   // @ts-ignore |   // @ts-ignore | ||||||
|   // middleware: process.env.GLOBAL_MIDDLEWARE, |   middleware: "auth", | ||||||
|   setup() { |   setup() { | ||||||
|     const { cookbooks } = useCookbooks(); |     const { cookbooks } = useCookbooks(); | ||||||
|     // @ts-ignore |     // @ts-ignore | ||||||
|     const { $globals } = useContext(); |     const { $globals, $auth } = useContext(); | ||||||
|  |  | ||||||
|  |     const isAdmin = computed(() => $auth.user?.admin); | ||||||
|  |  | ||||||
|     const cookbookLinks = computed(() => { |     const cookbookLinks = computed(() => { | ||||||
|       if (!cookbooks.value) return []; |       if (!cookbooks.value) return []; | ||||||
| @@ -53,7 +55,7 @@ export default defineComponent({ | |||||||
|         }; |         }; | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|     return { cookbookLinks }; |     return { cookbookLinks, isAdmin }; | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ export default { | |||||||
|     // https://go.nuxtjs.dev/pwa |     // https://go.nuxtjs.dev/pwa | ||||||
|     "@nuxtjs/pwa", |     "@nuxtjs/pwa", | ||||||
|     // https://i18n.nuxtjs.org/setup |     // https://i18n.nuxtjs.org/setup | ||||||
|     "nuxt-i18n", |     "@nuxtjs/i18n", | ||||||
|     // https://auth.nuxtjs.org/guide/setup |     // https://auth.nuxtjs.org/guide/setup | ||||||
|     "@nuxtjs/auth-next", |     "@nuxtjs/auth-next", | ||||||
|     // https://github.com/nuxt-community/proxy-module |     // https://github.com/nuxt-community/proxy-module | ||||||
| @@ -81,8 +81,8 @@ export default { | |||||||
|  |  | ||||||
|   auth: { |   auth: { | ||||||
|     redirect: { |     redirect: { | ||||||
|       login: "/user/login", |       login: "/login", | ||||||
|       logout: "/", |       logout: "/login", | ||||||
|       callback: "/login", |       callback: "/login", | ||||||
|       home: "/", |       home: "/", | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ | |||||||
|     "@mdi/js": "^5.9.55", |     "@mdi/js": "^5.9.55", | ||||||
|     "@nuxtjs/auth-next": "5.0.0-1624817847.21691f1", |     "@nuxtjs/auth-next": "5.0.0-1624817847.21691f1", | ||||||
|     "@nuxtjs/axios": "^5.13.6", |     "@nuxtjs/axios": "^5.13.6", | ||||||
|  |     "@nuxtjs/i18n": "^7.0.3", | ||||||
|     "@nuxtjs/proxy": "^2.1.0", |     "@nuxtjs/proxy": "^2.1.0", | ||||||
|     "@nuxtjs/pwa": "^3.3.5", |     "@nuxtjs/pwa": "^3.3.5", | ||||||
|     "@vue/composition-api": "^1.0.5", |     "@vue/composition-api": "^1.0.5", | ||||||
| @@ -25,7 +26,6 @@ | |||||||
|     "core-js": "^3.15.1", |     "core-js": "^3.15.1", | ||||||
|     "fuse.js": "^6.4.6", |     "fuse.js": "^6.4.6", | ||||||
|     "nuxt": "^2.15.7", |     "nuxt": "^2.15.7", | ||||||
|     "nuxt-i18n": "^6.28.0", |  | ||||||
|     "vuedraggable": "^2.24.3", |     "vuedraggable": "^2.24.3", | ||||||
|     "vuetify": "^2.5.5" |     "vuetify": "^2.5.5" | ||||||
|   }, |   }, | ||||||
| @@ -33,7 +33,7 @@ | |||||||
|     "@babel/eslint-parser": "^7.14.7", |     "@babel/eslint-parser": "^7.14.7", | ||||||
|     "@nuxt/types": "^2.15.7", |     "@nuxt/types": "^2.15.7", | ||||||
|     "@nuxt/typescript-build": "^2.1.0", |     "@nuxt/typescript-build": "^2.1.0", | ||||||
|     "@nuxtjs/composition-api": "^0.26.0", |     "@nuxtjs/composition-api": "^0.28.0", | ||||||
|     "@nuxtjs/eslint-config-typescript": "^6.0.1", |     "@nuxtjs/eslint-config-typescript": "^6.0.1", | ||||||
|     "@nuxtjs/eslint-module": "^3.0.2", |     "@nuxtjs/eslint-module": "^3.0.2", | ||||||
|     "@nuxtjs/vuetify": "^1.12.1", |     "@nuxtjs/vuetify": "^1.12.1", | ||||||
| @@ -50,4 +50,4 @@ | |||||||
|   "resolutions": { |   "resolutions": { | ||||||
|     "vite": "2.3.8" |     "vite": "2.3.8" | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -95,8 +95,8 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import AdminBackupImportOptions from "@/components/Domain/Admin/AdminBackupImportOptions.vue"; |  | ||||||
| import { defineComponent, reactive, toRefs, useContext, ref } from "@nuxtjs/composition-api"; | import { defineComponent, reactive, toRefs, useContext, ref } from "@nuxtjs/composition-api"; | ||||||
|  | import AdminBackupImportOptions from "@/components/Domain/Admin/AdminBackupImportOptions.vue"; | ||||||
| import { useBackups } from "~/composables/use-backups"; | import { useBackups } from "~/composables/use-backups"; | ||||||
|  |  | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|   | |||||||
| @@ -23,8 +23,8 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue"; |  | ||||||
| import { defineComponent, useRoute, ref } from "@nuxtjs/composition-api"; | import { defineComponent, useRoute, ref } from "@nuxtjs/composition-api"; | ||||||
|  | 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({ | ||||||
|   components: { RecipeCardSection }, |   components: { RecipeCardSection }, | ||||||
|   | |||||||
| @@ -178,10 +178,9 @@ | |||||||
|           </v-btn> |           </v-btn> | ||||||
|         </v-form> |         </v-form> | ||||||
|       </v-card-text> |       </v-card-text> | ||||||
|       <v-btn v-if="$config.ALLOW_SIGNUP" class="mx-auto" text to="/user/sign-up"> Sign Up </v-btn> |       <v-btn v-if="$config.ALLOW_SIGNUP" class="mx-auto" text to="/register"> Register </v-btn> | ||||||
|       <v-btn v-else class="mx-auto" text disabled> Invite Only </v-btn> |       <v-btn v-else class="mx-auto" text disabled> Invite Only </v-btn> | ||||||
|     </v-card> |     </v-card> | ||||||
|     <!-- <v-col class="fill-height"> </v-col> --> |  | ||||||
|   </v-container> |   </v-container> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
							
								
								
									
										170
									
								
								frontend/pages/register.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								frontend/pages/register.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | |||||||
|  | <template> | ||||||
|  |   <v-container fill-height fluid class="d-flex justify-center align-start narrow-container"> | ||||||
|  |     <v-card color="background d-flex flex-column align-center" flat width="700px"> | ||||||
|  |       <v-card-title class="headline"> User Registration </v-card-title> | ||||||
|  |       <v-card-text> | ||||||
|  |         <v-form ref="domRegisterForm" @submit.prevent="register()"> | ||||||
|  |           <ToggleState> | ||||||
|  |             <template #activator="{ toggle }"> | ||||||
|  |               <div class="d-flex justify-center my-2"> | ||||||
|  |                 <v-btn-toggle tile mandatory group color="primary"> | ||||||
|  |                   <v-btn small @click="toggle(false)"> Create a Group </v-btn> | ||||||
|  |                   <v-btn small @click="toggle(true)"> Join a Group </v-btn> | ||||||
|  |                 </v-btn-toggle> | ||||||
|  |               </div> | ||||||
|  |             </template> | ||||||
|  |             <template #default="{ state }"> | ||||||
|  |               <v-text-field | ||||||
|  |                 v-if="!state" | ||||||
|  |                 v-model="form.group" | ||||||
|  |                 filled | ||||||
|  |                 rounded | ||||||
|  |                 autofocus | ||||||
|  |                 validate-on-blur | ||||||
|  |                 class="rounded-lg" | ||||||
|  |                 :prepend-icon="$globals.icons.group" | ||||||
|  |                 :rules="[tokenOrGroup]" | ||||||
|  |                 label="New Group Name" | ||||||
|  |               /> | ||||||
|  |               <v-text-field | ||||||
|  |                 v-else | ||||||
|  |                 v-model="form.groupToken" | ||||||
|  |                 filled | ||||||
|  |                 rounded | ||||||
|  |                 validate-on-blur | ||||||
|  |                 :rules="[tokenOrGroup]" | ||||||
|  |                 class="rounded-lg" | ||||||
|  |                 :prepend-icon="$globals.icons.group" | ||||||
|  |                 label="Group Token" | ||||||
|  |               /> | ||||||
|  |             </template> | ||||||
|  |           </ToggleState> | ||||||
|  |           <v-text-field | ||||||
|  |             v-model="form.email" | ||||||
|  |             filled | ||||||
|  |             rounded | ||||||
|  |             class="rounded-lg" | ||||||
|  |             validate-on-blur | ||||||
|  |             :prepend-icon="$globals.icons.email" | ||||||
|  |             label="Email" | ||||||
|  |             :rules="[validators.required, validators.email]" | ||||||
|  |           /> | ||||||
|  |           <v-text-field | ||||||
|  |             v-model="form.username" | ||||||
|  |             filled | ||||||
|  |             rounded | ||||||
|  |             class="rounded-lg" | ||||||
|  |             :prepend-icon="$globals.icons.user" | ||||||
|  |             label="Username" | ||||||
|  |             :rules="[validators.required]" | ||||||
|  |           /> | ||||||
|  |           <v-text-field | ||||||
|  |             v-model="form.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="form.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]" | ||||||
|  |           /> | ||||||
|  |           <div class="mt-n4 px-8"> | ||||||
|  |             <v-checkbox v-model="form.private" label="Keep My Recipes Private"></v-checkbox> | ||||||
|  |             <p class="text-caption mt-n4"> | ||||||
|  |               Sets your group and all recipes defaults to private. You can always change this later. | ||||||
|  |             </p> | ||||||
|  |             <v-checkbox v-model="form.advanced" label="Enable Advanced Content"></v-checkbox> | ||||||
|  |             <p class="text-caption mt-n4"> | ||||||
|  |               Enables advanced features like Recipe Scaling, API keys, Webhooks, and Data Management. Don't worry, you | ||||||
|  |               can always change this later | ||||||
|  |             </p> | ||||||
|  |           </div> | ||||||
|  |           <div class="d-flex flex-column justify-center"> | ||||||
|  |             <v-btn :loading="loggingIn" color="primary" type="submit" large rounded class="rounded-xl" block> | ||||||
|  |               Register | ||||||
|  |             </v-btn> | ||||||
|  |             <v-btn class="mx-auto my-2" text to="/login"> Login </v-btn> | ||||||
|  |           </div> | ||||||
|  |         </v-form> | ||||||
|  |       </v-card-text> | ||||||
|  |     </v-card> | ||||||
|  |   </v-container> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import { computed, defineComponent, reactive, toRefs, ref, useRouter } from "@nuxtjs/composition-api"; | ||||||
|  | import { validators } from "@/composables/use-validators"; | ||||||
|  | import { useApiSingleton } from "~/composables/use-api"; | ||||||
|  | import { alert } from "~/composables/use-toast"; | ||||||
|  |  | ||||||
|  | export default defineComponent({ | ||||||
|  |   layout: "basic", | ||||||
|  |   setup() { | ||||||
|  |     const api = useApiSingleton(); | ||||||
|  |     const state = reactive({ | ||||||
|  |       loggingIn: false, | ||||||
|  |       success: false, | ||||||
|  |     }); | ||||||
|  |     const allowSignup = computed(() => process.env.AllOW_SIGNUP); | ||||||
|  |  | ||||||
|  |     const router = useRouter(); | ||||||
|  |  | ||||||
|  |     // @ts-ignore | ||||||
|  |     const domRegisterForm = ref<VForm>(null); | ||||||
|  |  | ||||||
|  |     const form = reactive({ | ||||||
|  |       group: "", | ||||||
|  |       groupToken: "", | ||||||
|  |       email: "", | ||||||
|  |       username: "", | ||||||
|  |       password: "", | ||||||
|  |       passwordConfirm: "", | ||||||
|  |       advanced: false, | ||||||
|  |       private: false, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const passwordMatch = () => form.password === form.passwordConfirm || "Passwords do not match"; | ||||||
|  |     const tokenOrGroup = () => form.group !== "" || form.groupToken !== "" || "Group name or token must be given"; | ||||||
|  |  | ||||||
|  |     async function register() { | ||||||
|  |       if (!domRegisterForm.value?.validate()) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const { data, response } = await api.register.register(form); | ||||||
|  |  | ||||||
|  |       if (response?.status === 201) { | ||||||
|  |         state.success = true; | ||||||
|  |         alert.success("Registration Success"); | ||||||
|  |         router.push("/user/login"); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       console.log(data, response); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       domRegisterForm, | ||||||
|  |       validators, | ||||||
|  |       allowSignup, | ||||||
|  |       form, | ||||||
|  |       ...toRefs(state), | ||||||
|  |       passwordMatch, | ||||||
|  |       tokenOrGroup, | ||||||
|  |       register, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
| @@ -50,8 +50,8 @@ | |||||||
|      |      | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from "@nuxtjs/composition-api"; | import { defineComponent } from "@nuxtjs/composition-api"; | ||||||
| import { useCookbooks } from "@/composables/use-group-cookbooks"; |  | ||||||
| import draggable from "vuedraggable"; | import draggable from "vuedraggable"; | ||||||
|  | import { useCookbooks } from "@/composables/use-group-cookbooks"; | ||||||
|  |  | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|   components: { draggable }, |   components: { draggable }, | ||||||
|   | |||||||
| @@ -1,39 +1,139 @@ | |||||||
| <template> | <template> | ||||||
|   <v-container> |   <v-container class="narrow-container"> | ||||||
|     <BasePageTitle divider> |     <BasePageTitle class="mb-5"> | ||||||
|       <template #header> |       <template #header> | ||||||
|         <v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img> |         <v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img> | ||||||
|       </template> |       </template> | ||||||
|       <template #title> Group Settings </template> |       <template #title> Group Settings </template> | ||||||
|       These items are shared within your group. Editing one of them will change it for the whole group! |       These items are shared within your group. Editing one of them will change it for the whole group! | ||||||
|     </BasePageTitle> |     </BasePageTitle> | ||||||
|     <v-card tag="section" outlined> |     <section> | ||||||
|       <v-card-text> |       <BaseCardSectionTitle title="Mealplan Categories"> | ||||||
|         <BaseCardSectionTitle title="Mealplan Categories"> |         Set the categories below for the ones that you want to be included in your mealplan random generation. | ||||||
|           Set the categories below for the ones that you want to be included in your mealplan random generation. |       </BaseCardSectionTitle> | ||||||
|           <div class="mt-2"> |       <DomainRecipeCategoryTagSelector v-if="categories" v-model="categories" /> | ||||||
|             <BaseButton save @click="actions.updateAll()" /> |       <v-card-actions> | ||||||
|           </div> |         <v-spacer></v-spacer> | ||||||
|         </BaseCardSectionTitle> |         <BaseButton save @click="actions.updateAll()" /> | ||||||
|         <DomainRecipeCategoryTagSelector v-if="categories" v-model="categories" /> |       </v-card-actions> | ||||||
|       </v-card-text> |     </section> | ||||||
|     </v-card> |  | ||||||
|  |     <section v-if="group"> | ||||||
|  |       <BaseCardSectionTitle class="mt-10" title="Group Preferences"></BaseCardSectionTitle> | ||||||
|  |       <v-checkbox | ||||||
|  |         v-model="group.preferences.privateGroup" | ||||||
|  |         class="mt-n4" | ||||||
|  |         label="Private Group" | ||||||
|  |         @change="groupActions.updatePreferences()" | ||||||
|  |       ></v-checkbox> | ||||||
|  |       <v-select | ||||||
|  |         v-model="group.preferences.firstDayOfWeek" | ||||||
|  |         :prepend-icon="$globals.icons.calendarWeekBegin" | ||||||
|  |         :items="allDays" | ||||||
|  |         item-text="name" | ||||||
|  |         item-value="value" | ||||||
|  |         :label="$t('settings.first-day-of-week')" | ||||||
|  |         @change="groupActions.updatePreferences()" | ||||||
|  |       /> | ||||||
|  |     </section> | ||||||
|  |  | ||||||
|  |     <section v-if="group"> | ||||||
|  |       <BaseCardSectionTitle class="mt-10" title="Default Recipe Preferences"> | ||||||
|  |         These are the default settings when a new recipe is created in your group. These can be changed for indivdual | ||||||
|  |         recipes in the recipe settings menu. | ||||||
|  |       </BaseCardSectionTitle> | ||||||
|  |  | ||||||
|  |       <v-checkbox | ||||||
|  |         v-model="group.preferences.recipePublic" | ||||||
|  |         class="mt-n4" | ||||||
|  |         label="Allow users outside of your group to see your recipes" | ||||||
|  |         @change="groupActions.updatePreferences()" | ||||||
|  |       ></v-checkbox> | ||||||
|  |       <v-checkbox | ||||||
|  |         v-model="group.preferences.recipeShowNutrition" | ||||||
|  |         class="mt-n4" | ||||||
|  |         label="Show nutrition information" | ||||||
|  |         @change="groupActions.updatePreferences()" | ||||||
|  |       ></v-checkbox> | ||||||
|  |       <v-checkbox | ||||||
|  |         v-model="group.preferences.recipeShowAssets" | ||||||
|  |         class="mt-n4" | ||||||
|  |         label="Show recipe assets" | ||||||
|  |         @change="groupActions.updatePreferences()" | ||||||
|  |       ></v-checkbox> | ||||||
|  |       <v-checkbox | ||||||
|  |         v-model="group.preferences.recipeLandscapeView" | ||||||
|  |         class="mt-n4" | ||||||
|  |         label="Default to landscape view" | ||||||
|  |         @change="groupActions.updatePreferences()" | ||||||
|  |       ></v-checkbox> | ||||||
|  |       <v-checkbox | ||||||
|  |         v-model="group.preferences.recipeDisableComments" | ||||||
|  |         class="mt-n4" | ||||||
|  |         label="Allow recipe comments from users in your group" | ||||||
|  |         @change="groupActions.updatePreferences()" | ||||||
|  |       ></v-checkbox> | ||||||
|  |       <v-checkbox | ||||||
|  |         v-model="group.preferences.recipeDisableAmount" | ||||||
|  |         class="mt-n4" | ||||||
|  |         label="Enable organizing recipe ingredients by units and food" | ||||||
|  |         @change="groupActions.updatePreferences()" | ||||||
|  |       ></v-checkbox> | ||||||
|  |     </section> | ||||||
|   </v-container> |   </v-container> | ||||||
| </template> | </template> | ||||||
|      |      | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from "@nuxtjs/composition-api"; | import { defineComponent, useContext } from "@nuxtjs/composition-api"; | ||||||
| import { useGroup } from "~/composables/use-groups"; | import { useGroupCategories, useGroupSelf } from "~/composables/use-groups"; | ||||||
|  |  | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|   setup() { |   setup() { | ||||||
|     const { categories, actions } = useGroup(); |     const { categories, actions } = useGroupCategories(); | ||||||
|  |     const { group, actions: groupActions } = useGroupSelf(); | ||||||
|  |  | ||||||
|  |     const { i18n } = useContext(); | ||||||
|  |  | ||||||
|  |     const allDays = [ | ||||||
|  |       { | ||||||
|  |         name: i18n.t("general.sunday"), | ||||||
|  |         value: 0, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: i18n.t("general.monday"), | ||||||
|  |         value: 1, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: i18n.t("general.tuesday"), | ||||||
|  |         value: 2, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: i18n.t("general.wednesday"), | ||||||
|  |         value: 3, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: i18n.t("general.thursday"), | ||||||
|  |         value: 4, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: i18n.t("general.friday"), | ||||||
|  |         value: 5, | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: i18n.t("general.saturday"), | ||||||
|  |         value: 6, | ||||||
|  |       }, | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|       categories, |       categories, | ||||||
|       actions, |       actions, | ||||||
|  |       group, | ||||||
|  |       groupActions, | ||||||
|  |       allDays, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|      |      | ||||||
|  |  | ||||||
|   | |||||||
| @@ -82,6 +82,18 @@ | |||||||
|         </template> |         </template> | ||||||
|       </ToggleState> |       </ToggleState> | ||||||
|     </section> |     </section> | ||||||
|  |     <section> | ||||||
|  |       <BaseCardSectionTitle class="mt-10" title="Preferences"> </BaseCardSectionTitle> | ||||||
|  |       <v-checkbox | ||||||
|  |         v-model="userCopy.advanced" | ||||||
|  |         class="mt-n4" | ||||||
|  |         label="Show advanced features (API Keys, Webhooks, and Data Management)" | ||||||
|  |         @change="updateUser" | ||||||
|  |       ></v-checkbox> | ||||||
|  |       <div class="d-flex justify-center mt-5"> | ||||||
|  |         <v-btn outlined class="rounded-xl" to="/user/group"> Looking for Privacy Settings? </v-btn> | ||||||
|  |       </div> | ||||||
|  |     </section> | ||||||
|   </v-container> |   </v-container> | ||||||
| </template> | </template> | ||||||
|      |      | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ | |||||||
|         </v-col> |         </v-col> | ||||||
|         <v-col cols="12" sm="12" md="6"> |         <v-col cols="12" sm="12" md="6"> | ||||||
|           <UserProfileLinkCard |           <UserProfileLinkCard | ||||||
|  |             v-if="user.advanced" | ||||||
|             :link="{ text: 'Manage Your API Tokens', to: '/user/profile/api-tokens' }" |             :link="{ text: 'Manage Your API Tokens', to: '/user/profile/api-tokens' }" | ||||||
|             :image="require('~/static/svgs/manage-api-tokens.svg')" |             :image="require('~/static/svgs/manage-api-tokens.svg')" | ||||||
|           > |           > | ||||||
| @@ -63,6 +64,7 @@ | |||||||
|         </v-col> |         </v-col> | ||||||
|         <v-col cols="12" sm="12" md="6"> |         <v-col cols="12" sm="12" md="6"> | ||||||
|           <UserProfileLinkCard |           <UserProfileLinkCard | ||||||
|  |             v-if="user.advanced" | ||||||
|             :link="{ text: 'Manage Webhooks', to: '/user/group/webhooks' }" |             :link="{ text: 'Manage Webhooks', to: '/user/group/webhooks' }" | ||||||
|             :image="require('~/static/svgs/manage-webhooks.svg')" |             :image="require('~/static/svgs/manage-webhooks.svg')" | ||||||
|           > |           > | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ | |||||||
|       "~/*": ["./*"], |       "~/*": ["./*"], | ||||||
|       "@/*": ["./*"] |       "@/*": ["./*"] | ||||||
|     }, |     }, | ||||||
|     "types": ["@nuxt/types", "@nuxtjs/axios", "@types/node", "nuxt-i18n", "@nuxtjs/auth-next"] |     "types": ["@nuxt/types", "@nuxtjs/axios", "@types/node", "@nuxtjs/i18n", "@nuxtjs/auth-next"] | ||||||
|   }, |   }, | ||||||
|   "exclude": ["node_modules", ".nuxt", "dist"] |   "exclude": ["node_modules", ".nuxt", "dist"] | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								frontend/types/vue.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								frontend/types/vue.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,14 +1,17 @@ | |||||||
| import Vue from "vue"; | import Vue from "vue"; | ||||||
|  | import "@nuxt/types"; | ||||||
|  |  | ||||||
| declare module "vue/types/vue" { | declare module "vue/types/vue" { | ||||||
|  |   interface Vue { | ||||||
|     interface Vue { |     $globals: any; | ||||||
|         $globals: any; |   } | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| declare module "vue/types/options" { | declare module "vue/types/options" { | ||||||
|     interface ComponentOptions<V extends Vue> { |   interface ComponentOptions<V extends Vue> { | ||||||
|         $globals?: any; |     $globals?: any; | ||||||
|     } |   } | ||||||
|  |   interface ComponentOptions<V extends UseContextReturn> { | ||||||
|  |     $globals?: any; | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										1267
									
								
								frontend/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										1267
									
								
								frontend/yarn.lock
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,6 +6,7 @@ from mealie.db.data_access_layer.group_access_model import GroupDataAccessModel | |||||||
| from mealie.db.models.event import Event, EventNotification | from mealie.db.models.event import Event, EventNotification | ||||||
| from mealie.db.models.group import Group | from mealie.db.models.group import Group | ||||||
| from mealie.db.models.group.cookbook import CookBook | from mealie.db.models.group.cookbook import CookBook | ||||||
|  | from mealie.db.models.group.preferences import GroupPreferencesModel | ||||||
| from mealie.db.models.group.shopping_list import ShoppingList | from mealie.db.models.group.shopping_list import ShoppingList | ||||||
| from mealie.db.models.group.webhooks import GroupWebhooksModel | from mealie.db.models.group.webhooks import GroupWebhooksModel | ||||||
| from mealie.db.models.mealplan import MealPlan | from mealie.db.models.mealplan import MealPlan | ||||||
| @@ -20,6 +21,7 @@ from mealie.schema.admin import SiteSettings as SiteSettingsSchema | |||||||
| from mealie.schema.cookbook import ReadCookBook | from mealie.schema.cookbook import ReadCookBook | ||||||
| from mealie.schema.events import Event as EventSchema | from mealie.schema.events import Event as EventSchema | ||||||
| from mealie.schema.events import EventNotificationIn | from mealie.schema.events import EventNotificationIn | ||||||
|  | from mealie.schema.group.group_preferences import ReadGroupPreferences | ||||||
| from mealie.schema.group.webhook import ReadWebhook | from mealie.schema.group.webhook import ReadWebhook | ||||||
| from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut | from mealie.schema.meal_plan import MealPlanOut, ShoppingListOut | ||||||
| from mealie.schema.recipe import ( | from mealie.schema.recipe import ( | ||||||
| @@ -89,3 +91,4 @@ class DatabaseAccessLayer: | |||||||
|         self.webhooks = BaseAccessModel(pk_id, GroupWebhooksModel, ReadWebhook) |         self.webhooks = BaseAccessModel(pk_id, GroupWebhooksModel, ReadWebhook) | ||||||
|         self.shopping_lists = BaseAccessModel(pk_id, ShoppingList, ShoppingListOut) |         self.shopping_lists = BaseAccessModel(pk_id, ShoppingList, ShoppingListOut) | ||||||
|         self.cookbooks = BaseAccessModel(pk_id, CookBook, ReadCookBook) |         self.cookbooks = BaseAccessModel(pk_id, CookBook, ReadCookBook) | ||||||
|  |         self.group_preferences = BaseAccessModel("group_id", GroupPreferencesModel, ReadGroupPreferences) | ||||||
|   | |||||||
| @@ -24,13 +24,11 @@ def default_recipe_unit_init(db: DatabaseAccessLayer, session: Session) -> None: | |||||||
|     for unit in get_default_units(): |     for unit in get_default_units(): | ||||||
|         try: |         try: | ||||||
|             db.ingredient_units.create(session, unit) |             db.ingredient_units.create(session, unit) | ||||||
|             print("Ingredient Unit Created") |  | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             print(e) |             print(e) | ||||||
|  |  | ||||||
|     for food in get_default_foods(): |     for food in get_default_foods(): | ||||||
|         try: |         try: | ||||||
|             db.ingredient_foods.create(session, food) |             db.ingredient_foods.create(session, food) | ||||||
|             print("Ingredient Food Created") |  | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             print(e) |             print(e) | ||||||
|   | |||||||
| @@ -8,7 +8,9 @@ from mealie.db.database import db | |||||||
| from mealie.db.db_setup import create_session, engine | from mealie.db.db_setup import create_session, engine | ||||||
| from mealie.db.models._model_base import SqlAlchemyBase | from mealie.db.models._model_base import SqlAlchemyBase | ||||||
| from mealie.schema.admin import SiteSettings | from mealie.schema.admin import SiteSettings | ||||||
|  | from mealie.schema.user.user import GroupBase | ||||||
| from mealie.services.events import create_general_event | from mealie.services.events import create_general_event | ||||||
|  | from mealie.services.group_services.group_mixins import create_new_group | ||||||
|  |  | ||||||
| logger = root_logger.get_logger("init_db") | logger = root_logger.get_logger("init_db") | ||||||
|  |  | ||||||
| @@ -38,9 +40,8 @@ def default_settings_init(session: Session): | |||||||
|  |  | ||||||
|  |  | ||||||
| def default_group_init(session: Session): | def default_group_init(session: Session): | ||||||
|     default_group = {"name": settings.DEFAULT_GROUP} |  | ||||||
|     logger.info("Generating Default Group") |     logger.info("Generating Default Group") | ||||||
|     db.groups.create(session, default_group) |     create_new_group(session, GroupBase(name=settings.DEFAULT_GROUP)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def default_user_init(session: Session): | def default_user_init(session: Session): | ||||||
|   | |||||||
| @@ -82,8 +82,6 @@ def auto_init(exclude: Union[set, list] = None):  # sourcery no-metrics | |||||||
|                     except Exception: |                     except Exception: | ||||||
|                         get_attr = "id" |                         get_attr = "id" | ||||||
|  |  | ||||||
|                     print(get_attr) |  | ||||||
|  |  | ||||||
|                     if relation_dir == ONETOMANY.name and use_list: |                     if relation_dir == ONETOMANY.name and use_list: | ||||||
|                         instances = handle_one_to_many_list(get_attr, relation_cls, val) |                         instances = handle_one_to_many_list(get_attr, relation_cls, val) | ||||||
|                         setattr(self, key, instances) |                         setattr(self, key, instances) | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ from .._model_utils import auto_init | |||||||
| from ..group.webhooks import GroupWebhooksModel | from ..group.webhooks import GroupWebhooksModel | ||||||
| from ..recipe.category import Category, group2categories | from ..recipe.category import Category, group2categories | ||||||
| from .cookbook import CookBook | from .cookbook import CookBook | ||||||
|  | from .preferences import GroupPreferencesModel | ||||||
|  |  | ||||||
|  |  | ||||||
| class Group(SqlAlchemyBase, BaseMixins): | class Group(SqlAlchemyBase, BaseMixins): | ||||||
| @@ -16,7 +17,15 @@ class Group(SqlAlchemyBase, BaseMixins): | |||||||
|     id = sa.Column(sa.Integer, primary_key=True) |     id = sa.Column(sa.Integer, primary_key=True) | ||||||
|     name = sa.Column(sa.String, index=True, nullable=False, unique=True) |     name = sa.Column(sa.String, index=True, nullable=False, unique=True) | ||||||
|     users = orm.relationship("User", back_populates="group") |     users = orm.relationship("User", back_populates="group") | ||||||
|     categories = orm.relationship(Category, secondary=group2categories, single_parent=True) |     categories = orm.relationship(Category, secondary=group2categories, single_parent=True, uselist=True) | ||||||
|  |  | ||||||
|  |     preferences = orm.relationship( | ||||||
|  |         GroupPreferencesModel, | ||||||
|  |         back_populates="group", | ||||||
|  |         uselist=False, | ||||||
|  |         single_parent=True, | ||||||
|  |         cascade="all, delete-orphan", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     # CRUD From Others |     # CRUD From Others | ||||||
|     mealplans = orm.relationship("MealPlan", back_populates="group", single_parent=True, order_by="MealPlan.start_date") |     mealplans = orm.relationship("MealPlan", back_populates="group", single_parent=True, order_by="MealPlan.start_date") | ||||||
| @@ -24,7 +33,7 @@ class Group(SqlAlchemyBase, BaseMixins): | |||||||
|     cookbooks = orm.relationship(CookBook, back_populates="group", single_parent=True) |     cookbooks = orm.relationship(CookBook, back_populates="group", single_parent=True) | ||||||
|     shopping_lists = orm.relationship("ShoppingList", back_populates="group", single_parent=True) |     shopping_lists = orm.relationship("ShoppingList", back_populates="group", single_parent=True) | ||||||
|  |  | ||||||
|     @auto_init({"users", "webhooks", "shopping_lists", "cookbooks"}) |     @auto_init({"users", "webhooks", "shopping_lists", "cookbooks", "preferences"}) | ||||||
|     def __init__(self, **_) -> None: |     def __init__(self, **_) -> None: | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								mealie/db/models/group/preferences.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								mealie/db/models/group/preferences.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | import sqlalchemy as sa | ||||||
|  | import sqlalchemy.orm as orm | ||||||
|  |  | ||||||
|  | from .._model_base import BaseMixins, SqlAlchemyBase | ||||||
|  | from .._model_utils import auto_init | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class GroupPreferencesModel(SqlAlchemyBase, BaseMixins): | ||||||
|  |     __tablename__ = "group_preferences" | ||||||
|  |     group_id = sa.Column(sa.Integer, sa.ForeignKey("groups.id")) | ||||||
|  |     group = orm.relationship("Group", back_populates="preferences") | ||||||
|  |  | ||||||
|  |     private_group: bool = sa.Column(sa.Boolean, default=True) | ||||||
|  |     first_day_of_week = sa.Column(sa.Integer, default=0) | ||||||
|  |  | ||||||
|  |     # Recipe Defaults | ||||||
|  |     recipe_public: bool = sa.Column(sa.Boolean, default=True) | ||||||
|  |     recipe_show_nutrition: bool = sa.Column(sa.Boolean, default=False) | ||||||
|  |     recipe_show_assets: bool = sa.Column(sa.Boolean, default=False) | ||||||
|  |     recipe_landscape_view: bool = sa.Column(sa.Boolean, default=False) | ||||||
|  |     recipe_disable_comments: bool = sa.Column(sa.Boolean, default=False) | ||||||
|  |     recipe_disable_amount: bool = sa.Column(sa.Boolean, default=False) | ||||||
|  |  | ||||||
|  |     @auto_init() | ||||||
|  |     def __init__(self, **_) -> None: | ||||||
|  |         pass | ||||||
| @@ -28,6 +28,7 @@ class User(SqlAlchemyBase, BaseMixins): | |||||||
|     email = Column(String, unique=True, index=True) |     email = Column(String, unique=True, index=True) | ||||||
|     password = Column(String) |     password = Column(String) | ||||||
|     admin = Column(Boolean, default=False) |     admin = Column(Boolean, default=False) | ||||||
|  |     advanced = Column(Boolean, default=False) | ||||||
|  |  | ||||||
|     group_id = Column(Integer, ForeignKey("groups.id")) |     group_id = Column(Integer, ForeignKey("groups.id")) | ||||||
|     group = orm.relationship("Group", back_populates="users") |     group = orm.relationship("Group", back_populates="users") | ||||||
| @@ -51,6 +52,7 @@ class User(SqlAlchemyBase, BaseMixins): | |||||||
|         favorite_recipes: list[str] = None, |         favorite_recipes: list[str] = None, | ||||||
|         group: str = settings.DEFAULT_GROUP, |         group: str = settings.DEFAULT_GROUP, | ||||||
|         admin=False, |         admin=False, | ||||||
|  |         advanced=False, | ||||||
|         **_ |         **_ | ||||||
|     ) -> None: |     ) -> None: | ||||||
|  |  | ||||||
| @@ -61,6 +63,7 @@ class User(SqlAlchemyBase, BaseMixins): | |||||||
|         self.group = Group.get_ref(session, group) |         self.group = Group.get_ref(session, group) | ||||||
|         self.admin = admin |         self.admin = admin | ||||||
|         self.password = password |         self.password = password | ||||||
|  |         self.advanced = advanced | ||||||
|  |  | ||||||
|         self.favorite_recipes = [ |         self.favorite_recipes = [ | ||||||
|             RecipeModel.get_ref(session=session, match_value=x, match_attr="slug") for x in favorite_recipes |             RecipeModel.get_ref(session=session, match_value=x, match_attr="slug") for x in favorite_recipes | ||||||
| @@ -69,13 +72,26 @@ class User(SqlAlchemyBase, BaseMixins): | |||||||
|         if self.username is None: |         if self.username is None: | ||||||
|             self.username = full_name |             self.username = full_name | ||||||
|  |  | ||||||
|     def update(self, full_name, email, group, admin, username, session=None, favorite_recipes=None, password=None, **_): |     def update( | ||||||
|  |         self, | ||||||
|  |         full_name, | ||||||
|  |         email, | ||||||
|  |         group, | ||||||
|  |         admin, | ||||||
|  |         username, | ||||||
|  |         session=None, | ||||||
|  |         favorite_recipes=None, | ||||||
|  |         password=None, | ||||||
|  |         advanced=False, | ||||||
|  |         **_ | ||||||
|  |     ): | ||||||
|         favorite_recipes = favorite_recipes or [] |         favorite_recipes = favorite_recipes or [] | ||||||
|         self.username = username |         self.username = username | ||||||
|         self.full_name = full_name |         self.full_name = full_name | ||||||
|         self.email = email |         self.email = email | ||||||
|         self.group = Group.get_ref(session, group) |         self.group = Group.get_ref(session, group) | ||||||
|         self.admin = admin |         self.admin = admin | ||||||
|  |         self.advanced = advanced | ||||||
|  |  | ||||||
|         if self.username is None: |         if self.username is None: | ||||||
|             self.username = full_name |             self.username = full_name | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| import operator | import operator | ||||||
| import shutil | import shutil | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from pprint import pprint |  | ||||||
|  |  | ||||||
| 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 | ||||||
| @@ -97,8 +96,6 @@ def import_database( | |||||||
|         rebase=import_data.rebase, |         rebase=import_data.rebase, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     pprint(db_import) |  | ||||||
|  |  | ||||||
|     background_tasks.add_task(create_backup_event, "Database Restore", f"Restore File: {file_name}", session) |     background_tasks.add_task(create_backup_event, "Database Restore", f"Restore File: {file_name}", session) | ||||||
|     return db_import |     return db_import | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| from fastapi import APIRouter | from fastapi import APIRouter | ||||||
|  |  | ||||||
| from mealie.services.base_http_service import RouterFactory | from mealie.services._base_http_service import RouterFactory | ||||||
| from mealie.services.cookbook.cookbook_service import CookbookService | from mealie.services.group_services import CookbookService, WebhookService | ||||||
| from mealie.services.group.webhook_service import WebhookService |  | ||||||
|  |  | ||||||
| from . import categories, crud, self_service | from . import categories, crud, self_service | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ from fastapi import Depends | |||||||
|  |  | ||||||
| from mealie.routes.routers import UserAPIRouter | from mealie.routes.routers import UserAPIRouter | ||||||
| from mealie.schema.recipe.recipe_category import CategoryBase | from mealie.schema.recipe.recipe_category import CategoryBase | ||||||
| from mealie.services.group.group_service import GroupSelfService | from mealie.services.group_services.group_service import GroupSelfService | ||||||
|  |  | ||||||
| user_router = UserAPIRouter(prefix="/groups/categories", tags=["Groups: Mealplan Categories"]) | user_router = UserAPIRouter(prefix="/groups/categories", tags=["Groups: Mealplan Categories"]) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,14 +1,27 @@ | |||||||
| from fastapi import Depends | from fastapi import Depends | ||||||
|  |  | ||||||
| from mealie.routes.routers import UserAPIRouter | from mealie.routes.routers import UserAPIRouter | ||||||
|  | from mealie.schema.group.group_preferences import ReadGroupPreferences, UpdateGroupPreferences | ||||||
| from mealie.schema.user.user import GroupInDB | from mealie.schema.user.user import GroupInDB | ||||||
| from mealie.services.group.group_service import GroupSelfService | from mealie.services.group_services.group_service import GroupSelfService | ||||||
|  |  | ||||||
| user_router = UserAPIRouter(prefix="/groups/self", tags=["Groups: Self Service"]) | user_router = UserAPIRouter(prefix="/groups", tags=["Groups: Self Service"]) | ||||||
|  |  | ||||||
|  |  | ||||||
| @user_router.get("", response_model=GroupInDB) | @user_router.get("/self", response_model=GroupInDB) | ||||||
| async def get_logged_in_user_group(g_self_service: GroupSelfService = Depends(GroupSelfService.write_existing)): | async def get_logged_in_user_group(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)): | ||||||
|     """ Returns the Group Data for the Current User """ |     """ Returns the Group Data for the Current User """ | ||||||
|  |  | ||||||
|     return g_self_service.item |     return g_service.item | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @user_router.put("/preferences", response_model=ReadGroupPreferences) | ||||||
|  | def update_group_preferences( | ||||||
|  |     new_pref: UpdateGroupPreferences, g_service: GroupSelfService = Depends(GroupSelfService.write_existing) | ||||||
|  | ): | ||||||
|  |     return g_service.update_preferences(new_pref).preferences | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @user_router.get("/preferences", response_model=ReadGroupPreferences) | ||||||
|  | def get_group_preferences(g_service: GroupSelfService = Depends(GroupSelfService.write_existing)): | ||||||
|  |     return g_service.item.preferences | ||||||
|   | |||||||
| @@ -1,12 +1,14 @@ | |||||||
| from fastapi import APIRouter | from fastapi import APIRouter | ||||||
|  |  | ||||||
| from . import api_tokens, crud, favorites, images, passwords, sign_up | from . import api_tokens, crud, favorites, images, passwords, registration, sign_up | ||||||
|  |  | ||||||
| # Must be used because of the way FastAPI works with nested routes | # Must be used because of the way FastAPI works with nested routes | ||||||
| user_prefix = "/users" | user_prefix = "/users" | ||||||
|  |  | ||||||
| router = APIRouter() | router = APIRouter() | ||||||
|  |  | ||||||
|  | router.include_router(registration.router, prefix=user_prefix, tags=["Users: Registration"]) | ||||||
|  |  | ||||||
| router.include_router(sign_up.admin_router, prefix=user_prefix, tags=["Users: Sign-Up"]) | router.include_router(sign_up.admin_router, prefix=user_prefix, tags=["Users: Sign-Up"]) | ||||||
| router.include_router(sign_up.public_router, prefix=user_prefix, tags=["Users: Sign-Up"]) | router.include_router(sign_up.public_router, prefix=user_prefix, tags=["Users: Sign-Up"]) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ from mealie.db.database import db | |||||||
| 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.services.user.user_service import UserService | from mealie.services.user_services import UserService | ||||||
|  |  | ||||||
| user_router = UserAPIRouter(prefix="") | user_router = UserAPIRouter(prefix="") | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								mealie/routes/users/registration.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								mealie/routes/users/registration.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | from fastapi import APIRouter, Depends, status | ||||||
|  |  | ||||||
|  | from mealie.schema.user.registration import CreateUserRegistration | ||||||
|  | from mealie.schema.user.user import UserOut | ||||||
|  | from mealie.services.user_services.registration_service import RegistrationService | ||||||
|  |  | ||||||
|  | router = APIRouter(prefix="/register") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED) | ||||||
|  | def reset_user_password( | ||||||
|  |     data: CreateUserRegistration, registration_service: RegistrationService = Depends(RegistrationService.public) | ||||||
|  | ): | ||||||
|  |     return registration_service.register_user(data) | ||||||
							
								
								
									
										25
									
								
								mealie/schema/group/group_preferences.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								mealie/schema/group/group_preferences.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | from fastapi_camelcase import CamelModel | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UpdateGroupPreferences(CamelModel): | ||||||
|  |     private_group: bool = False | ||||||
|  |     first_day_of_week: int = 0 | ||||||
|  |  | ||||||
|  |     # Recipe Defaults | ||||||
|  |     recipe_public: bool = True | ||||||
|  |     recipe_show_nutrition: bool = False | ||||||
|  |     recipe_show_assets: bool = False | ||||||
|  |     recipe_landscape_view: bool = False | ||||||
|  |     recipe_disable_comments: bool = False | ||||||
|  |     recipe_disable_amount: bool = False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CreateGroupPreferences(UpdateGroupPreferences): | ||||||
|  |     group_id: int | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ReadGroupPreferences(CreateGroupPreferences): | ||||||
|  |     id: int | ||||||
|  |  | ||||||
|  |     class Config: | ||||||
|  |         orm_mode = True | ||||||
							
								
								
									
										29
									
								
								mealie/schema/user/registration.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								mealie/schema/user/registration.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | from fastapi_camelcase import CamelModel | ||||||
|  | from pydantic import validator | ||||||
|  | from pydantic.types import constr | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CreateUserRegistration(CamelModel): | ||||||
|  |     group: str = None | ||||||
|  |     group_token: str = None | ||||||
|  |     email: constr(to_lower=True, strip_whitespace=True) | ||||||
|  |     username: constr(to_lower=True, strip_whitespace=True) | ||||||
|  |     password: str | ||||||
|  |     password_confirm: str | ||||||
|  |     advanced: bool = False | ||||||
|  |     private: bool = False | ||||||
|  |  | ||||||
|  |     @validator("password_confirm") | ||||||
|  |     @classmethod | ||||||
|  |     def passwords_match(cls, value, values): | ||||||
|  |         if "password" in values and value != values["password"]: | ||||||
|  |             raise ValueError("passwords do not match") | ||||||
|  |         return value | ||||||
|  |  | ||||||
|  |     @validator("group_token", always=True) | ||||||
|  |     @classmethod | ||||||
|  |     def group_or_token(cls, value, values): | ||||||
|  |         if bool(value) is False and bool(values["group"]) is False: | ||||||
|  |             raise ValueError("group or group_token must be provided") | ||||||
|  |  | ||||||
|  |         return value | ||||||
| @@ -6,8 +6,8 @@ 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 settings | ||||||
| from mealie.db.models.group import Group |  | ||||||
| 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.recipe import RecipeSummary | from mealie.schema.recipe import RecipeSummary | ||||||
|  |  | ||||||
| from ..meal_plan import MealPlanOut, ShoppingListOut | from ..meal_plan import MealPlanOut, ShoppingListOut | ||||||
| @@ -50,8 +50,9 @@ class UserBase(CamelModel): | |||||||
|     username: Optional[str] |     username: Optional[str] | ||||||
|     full_name: Optional[str] = None |     full_name: Optional[str] = None | ||||||
|     email: constr(to_lower=True, strip_whitespace=True) |     email: constr(to_lower=True, strip_whitespace=True) | ||||||
|     admin: bool |     admin: bool = False | ||||||
|     group: Optional[str] |     group: Optional[str] | ||||||
|  |     advanced: bool = False | ||||||
|     favorite_recipes: Optional[list[str]] = [] |     favorite_recipes: Optional[list[str]] = [] | ||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
| @@ -128,16 +129,11 @@ class GroupInDB(UpdateGroup): | |||||||
|     users: Optional[list[UserOut]] |     users: Optional[list[UserOut]] | ||||||
|     mealplans: Optional[list[MealPlanOut]] |     mealplans: Optional[list[MealPlanOut]] | ||||||
|     shopping_lists: Optional[list[ShoppingListOut]] |     shopping_lists: Optional[list[ShoppingListOut]] | ||||||
|  |     preferences: Optional[ReadGroupPreferences] = None | ||||||
|  |  | ||||||
|     class Config: |     class Config: | ||||||
|         orm_mode = True |         orm_mode = True | ||||||
|  |  | ||||||
|         @classmethod |  | ||||||
|         def getter_dict(_cls, orm_model: Group): |  | ||||||
|             return { |  | ||||||
|                 **GetterDict(orm_model), |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class LongLiveTokenInDB(CreateToken): | class LongLiveTokenInDB(CreateToken): | ||||||
|     id: int |     id: int | ||||||
|   | |||||||
| @@ -1 +0,0 @@ | |||||||
| from .cookbook_service import * |  | ||||||
| @@ -1,2 +1,3 @@ | |||||||
|  | from .cookbook_service import * | ||||||
| from .group_service import * | from .group_service import * | ||||||
| from .webhook_service import * | from .webhook_service import * | ||||||
| @@ -3,7 +3,7 @@ from __future__ import annotations | |||||||
| 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.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook | from mealie.schema.cookbook.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook | ||||||
| from mealie.services.base_http_service.http_services import UserHttpService | from mealie.services._base_http_service.http_services import UserHttpService | ||||||
| from mealie.services.events import create_group_event | from mealie.services.events import create_group_event | ||||||
| from mealie.utils.error_messages import ErrorMessages | from mealie.utils.error_messages import ErrorMessages | ||||||
| 
 | 
 | ||||||
							
								
								
									
										16
									
								
								mealie/services/group_services/group_mixins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mealie/services/group_services/group_mixins.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | from mealie.db.database import get_database | ||||||
|  | from mealie.schema.group.group_preferences import CreateGroupPreferences | ||||||
|  | from mealie.schema.user.user import GroupBase, GroupInDB | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_new_group(session, g_base: GroupBase, g_preferences: CreateGroupPreferences = None) -> GroupInDB: | ||||||
|  |     db = get_database() | ||||||
|  |     created_group = db.groups.create(session, g_base) | ||||||
|  |  | ||||||
|  |     g_preferences = g_preferences or CreateGroupPreferences(group_id=0) | ||||||
|  |  | ||||||
|  |     g_preferences.group_id = created_group.id | ||||||
|  |  | ||||||
|  |     db.group_preferences.create(session, g_preferences) | ||||||
|  |  | ||||||
|  |     return created_group | ||||||
| @@ -4,9 +4,10 @@ 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.schema.group.group_preferences import UpdateGroupPreferences | ||||||
| from mealie.schema.recipe.recipe_category import CategoryBase | from mealie.schema.recipe.recipe_category import CategoryBase | ||||||
| from mealie.schema.user.user import GroupInDB | from mealie.schema.user.user import GroupInDB | ||||||
| from mealie.services.base_http_service.http_services import UserHttpService | from mealie.services._base_http_service.http_services import UserHttpService | ||||||
| from mealie.services.events import create_group_event | from mealie.services.events import create_group_event | ||||||
| 
 | 
 | ||||||
| logger = get_logger(module=__name__) | logger = get_logger(module=__name__) | ||||||
| @@ -41,8 +42,11 @@ class GroupSelfService(UserHttpService[int, str]): | |||||||
|         return self.item |         return self.item | ||||||
| 
 | 
 | ||||||
|     def update_categories(self, new_categories: list[CategoryBase]): |     def update_categories(self, new_categories: list[CategoryBase]): | ||||||
|         if not self.item: |  | ||||||
|             return |  | ||||||
|         self.item.categories = new_categories |         self.item.categories = new_categories | ||||||
| 
 | 
 | ||||||
|         return self.db.groups.update(self.session, self.group_id, self.item) |         return self.db.groups.update(self.session, self.group_id, self.item) | ||||||
|  | 
 | ||||||
|  |     def update_preferences(self, new_preferences: UpdateGroupPreferences): | ||||||
|  |         self.db.group_preferences.update(self.session, self.group_id, new_preferences) | ||||||
|  | 
 | ||||||
|  |         return self.populate_item() | ||||||
| @@ -5,7 +5,7 @@ from fastapi import HTTPException, status | |||||||
| from mealie.core.root_logger import get_logger | from mealie.core.root_logger import get_logger | ||||||
| from mealie.schema.group import ReadWebhook | from mealie.schema.group import ReadWebhook | ||||||
| from mealie.schema.group.webhook import CreateWebhook, SaveWebhook | from mealie.schema.group.webhook import CreateWebhook, SaveWebhook | ||||||
| from mealie.services.base_http_service.http_services import UserHttpService | from mealie.services._base_http_service.http_services import UserHttpService | ||||||
| from mealie.services.events import create_group_event | from mealie.services.events import create_group_event | ||||||
| 
 | 
 | ||||||
| logger = get_logger(module=__name__) | logger = get_logger(module=__name__) | ||||||
| @@ -8,7 +8,7 @@ from sqlalchemy.exc import IntegrityError | |||||||
| from mealie.core.dependencies.grouped import PublicDeps, UserDeps | from mealie.core.dependencies.grouped import PublicDeps, UserDeps | ||||||
| from mealie.core.root_logger import get_logger | from mealie.core.root_logger import get_logger | ||||||
| from mealie.schema.recipe.recipe import CreateRecipe, Recipe | from mealie.schema.recipe.recipe import CreateRecipe, Recipe | ||||||
| from mealie.services.base_http_service.http_services import PublicHttpService | from mealie.services._base_http_service.http_services import PublicHttpService | ||||||
| from mealie.services.events import create_recipe_event | from mealie.services.events import create_recipe_event | ||||||
|  |  | ||||||
| logger = get_logger(module=__name__) | logger = get_logger(module=__name__) | ||||||
|   | |||||||
| @@ -55,7 +55,6 @@ def _exec_crf_test(input_text): | |||||||
|  |  | ||||||
|  |  | ||||||
| def convert_list_to_crf_model(list_of_ingrdeint_text: list[str]): | def convert_list_to_crf_model(list_of_ingrdeint_text: list[str]): | ||||||
|     print(list_of_ingrdeint_text) |  | ||||||
|     crf_output = _exec_crf_test([pre_process_string(x) for x in list_of_ingrdeint_text]) |     crf_output = _exec_crf_test([pre_process_string(x) for x in list_of_ingrdeint_text]) | ||||||
|     crf_models = [CRFIngredient(**ingredient) for ingredient in utils.import_data(crf_output.split("\n"))] |     crf_models = [CRFIngredient(**ingredient) for ingredient in utils.import_data(crf_output.split("\n"))] | ||||||
|  |  | ||||||
| @@ -82,6 +81,3 @@ def convert_crf_models_to_ingredients(crf_models: list[CRFIngredient]): | |||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     crf_models = convert_list_to_crf_model(INGREDIENT_TEXT) |     crf_models = convert_list_to_crf_model(INGREDIENT_TEXT) | ||||||
|     ingredients = convert_crf_models_to_ingredients(crf_models) |     ingredients = convert_crf_models_to_ingredients(crf_models) | ||||||
|  |  | ||||||
|     for ingredient in ingredients: |  | ||||||
|         print(ingredient.input) |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								mealie/services/user_services/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								mealie/services/user_services/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | from .user_service import * | ||||||
							
								
								
									
										61
									
								
								mealie/services/user_services/registration_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								mealie/services/user_services/registration_service.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | from mealie.core.root_logger import get_logger | ||||||
|  | from mealie.core.security import hash_password | ||||||
|  | from mealie.schema.group.group_preferences import CreateGroupPreferences | ||||||
|  | from mealie.schema.user.registration import CreateUserRegistration | ||||||
|  | from mealie.schema.user.user import GroupBase, GroupInDB, PrivateUser, UserIn | ||||||
|  | from mealie.services._base_http_service.http_services import PublicHttpService | ||||||
|  | from mealie.services.events import create_user_event | ||||||
|  | from mealie.services.group_services.group_mixins import create_new_group | ||||||
|  |  | ||||||
|  | logger = get_logger(module=__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RegistrationService(PublicHttpService[int, str]): | ||||||
|  |     event_func = create_user_event | ||||||
|  |  | ||||||
|  |     def populate_item() -> None: | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     def register_user(self, registration: CreateUserRegistration) -> PrivateUser: | ||||||
|  |         self.registration = registration | ||||||
|  |  | ||||||
|  |         logger.info(f"Registering user {registration.username}") | ||||||
|  |  | ||||||
|  |         if registration.group: | ||||||
|  |             group = self._create_new_group() | ||||||
|  |         else: | ||||||
|  |             group = self._existing_group_ref() | ||||||
|  |  | ||||||
|  |         return self._create_new_user(group) | ||||||
|  |  | ||||||
|  |     def _create_new_user(self, group: GroupInDB) -> PrivateUser: | ||||||
|  |         new_user = UserIn( | ||||||
|  |             email=self.registration.email, | ||||||
|  |             username=self.registration.username, | ||||||
|  |             password=hash_password(self.registration.password), | ||||||
|  |             full_name=self.registration.username, | ||||||
|  |             advanced=self.registration.advanced, | ||||||
|  |             group=group.name, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return self.db.users.create(self.session, new_user) | ||||||
|  |  | ||||||
|  |     def _create_new_group(self) -> GroupInDB: | ||||||
|  |         group_data = GroupBase(name=self.registration.group) | ||||||
|  |  | ||||||
|  |         group_preferences = CreateGroupPreferences( | ||||||
|  |             group_id=0, | ||||||
|  |             private_group=self.registration.private, | ||||||
|  |             first_day_of_week=0, | ||||||
|  |             recipe_public=not self.registration.private, | ||||||
|  |             recipe_show_nutrition=self.registration.advanced, | ||||||
|  |             recipe_show_assets=self.registration.advanced, | ||||||
|  |             recipe_landscape_view=False, | ||||||
|  |             recipe_disable_comments=self.registration.advanced, | ||||||
|  |             recipe_disable_amount=self.registration.advanced, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return create_new_group(self.session, group_data, group_preferences) | ||||||
|  |  | ||||||
|  |     def _existing_group_ref(self) -> GroupInDB: | ||||||
|  |         pass | ||||||
| @@ -3,7 +3,7 @@ from fastapi import HTTPException, status | |||||||
| from mealie.core.root_logger import get_logger | from mealie.core.root_logger import get_logger | ||||||
| from mealie.core.security import hash_password, verify_password | from mealie.core.security import hash_password, verify_password | ||||||
| from mealie.schema.user.user import ChangePassword, PrivateUser | from mealie.schema.user.user import ChangePassword, PrivateUser | ||||||
| from mealie.services.base_http_service.http_services import UserHttpService | from mealie.services._base_http_service.http_services import UserHttpService | ||||||
| from mealie.services.events import create_user_event | from mealie.services.events import create_user_event | ||||||
| 
 | 
 | ||||||
| logger = get_logger(module=__name__) | logger = get_logger(module=__name__) | ||||||
| @@ -1,43 +0,0 @@ | |||||||
| import json |  | ||||||
|  |  | ||||||
| import pytest |  | ||||||
| from fastapi.testclient import TestClient |  | ||||||
|  |  | ||||||
| from tests.app_routes import AppRoutes |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture() |  | ||||||
| def page_data(): |  | ||||||
|     return {"name": "My New Page", "description": "", "position": 0, "categories": [], "groupId": 1} |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_create_cookbook(api_client: TestClient, api_routes: AppRoutes, admin_token, page_data): |  | ||||||
|     response = api_client.post(api_routes.group_cookbook, json=page_data, headers=admin_token) |  | ||||||
|  |  | ||||||
|     assert response.status_code == 200 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_read_cookbook(api_client: TestClient, api_routes: AppRoutes, page_data, admin_token): |  | ||||||
|     response = api_client.get(api_routes.group_cookbook_id(1), headers=admin_token) |  | ||||||
|  |  | ||||||
|     page_data["id"] = 1 |  | ||||||
|     page_data["slug"] = "my-new-page" |  | ||||||
|  |  | ||||||
|     assert json.loads(response.text) == page_data |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_update_cookbook(api_client: TestClient, api_routes: AppRoutes, page_data, admin_token): |  | ||||||
|     page_data["id"] = 1 |  | ||||||
|     page_data["name"] = "My New Name" |  | ||||||
|     response = api_client.put(api_routes.group_cookbook_id(1), json=page_data, headers=admin_token) |  | ||||||
|  |  | ||||||
|     assert response.status_code == 200 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_delete_cookbook(api_client: TestClient, api_routes: AppRoutes, admin_token): |  | ||||||
|     response = api_client.delete(api_routes.group_cookbook_id(1), headers=admin_token) |  | ||||||
|  |  | ||||||
|     assert response.status_code == 200 |  | ||||||
|  |  | ||||||
|     response = api_client.get(api_routes.group_cookbook_id(1), headers=admin_token) |  | ||||||
|     assert response.status_code == 404 |  | ||||||
| @@ -4,6 +4,7 @@ import pytest | |||||||
| from fastapi.testclient import TestClient | from fastapi.testclient import TestClient | ||||||
|  |  | ||||||
| from tests.app_routes import AppRoutes | from tests.app_routes import AppRoutes | ||||||
|  | from tests.utils.assertion_helpers import assert_ignore_keys | ||||||
|  |  | ||||||
|  |  | ||||||
| @pytest.fixture | @pytest.fixture | ||||||
| @@ -41,8 +42,10 @@ def test_update_group(api_client: TestClient, api_routes: AppRoutes, admin_token | |||||||
|     # Validate Changes |     # Validate Changes | ||||||
|     response = api_client.get(api_routes.groups, headers=admin_token) |     response = api_client.get(api_routes.groups, headers=admin_token) | ||||||
|     all_groups = json.loads(response.text) |     all_groups = json.loads(response.text) | ||||||
|  |  | ||||||
|     id_2 = filter(lambda x: x["id"] == 2, all_groups) |     id_2 = filter(lambda x: x["id"] == 2, all_groups) | ||||||
|     assert next(id_2) == new_data |  | ||||||
|  |     assert_ignore_keys(new_data, next(id_2), ["preferences"]) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_home_group_not_deletable(api_client: TestClient, api_routes: AppRoutes, admin_token): | def test_home_group_not_deletable(api_client: TestClient, api_routes: AppRoutes, admin_token): | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ def backup_data(): | |||||||
|         "force": True, |         "force": True, | ||||||
|         "recipes": True, |         "recipes": True, | ||||||
|         "settings": False,  # ! Broken |         "settings": False,  # ! Broken | ||||||
|         "groups": True, |         "groups": False,  # ! Also Broken | ||||||
|         "users": True, |         "users": True, | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,46 @@ | |||||||
|  | import pytest | ||||||
|  | from fastapi.testclient import TestClient | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Routes: | ||||||
|  |     base = "/api/groups/cookbooks" | ||||||
|  |  | ||||||
|  |     def item(item_id: int) -> str: | ||||||
|  |         return f"{Routes.base}/{item_id}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.fixture() | ||||||
|  | def page_data(): | ||||||
|  |     return {"name": "My New Page", "description": "", "position": 0, "categories": [], "groupId": 1} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_create_cookbook(api_client: TestClient, admin_token, page_data): | ||||||
|  |     response = api_client.post(Routes.base, json=page_data, headers=admin_token) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_read_cookbook(api_client: TestClient, page_data, admin_token): | ||||||
|  |     response = api_client.get(Routes.item(1), headers=admin_token) | ||||||
|  |  | ||||||
|  |     page_data["id"] = 1 | ||||||
|  |     page_data["slug"] = "my-new-page" | ||||||
|  |  | ||||||
|  |     assert response.json() == page_data | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_update_cookbook(api_client: TestClient, page_data, admin_token): | ||||||
|  |     page_data["id"] = 1 | ||||||
|  |     page_data["name"] = "My New Name" | ||||||
|  |     response = api_client.put(Routes.item(1), json=page_data, headers=admin_token) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_delete_cookbook(api_client: TestClient, admin_token): | ||||||
|  |     response = api_client.delete(Routes.item(1), headers=admin_token) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |  | ||||||
|  |     response = api_client.get(Routes.item(1), headers=admin_token) | ||||||
|  |     assert response.status_code == 404 | ||||||
| @@ -0,0 +1,32 @@ | |||||||
|  | from fastapi.testclient import TestClient | ||||||
|  |  | ||||||
|  | from mealie.schema.user.registration import CreateUserRegistration | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Routes: | ||||||
|  |     base = "/api/users/register" | ||||||
|  |     auth_token = "/api/auth/token" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_user_registration_new_group(api_client: TestClient): | ||||||
|  |     registration = CreateUserRegistration( | ||||||
|  |         group="New Group Name", | ||||||
|  |         email="email@email.com", | ||||||
|  |         username="fake-user-name", | ||||||
|  |         password="fake-password", | ||||||
|  |         password_confirm="fake-password", | ||||||
|  |         advanced=False, | ||||||
|  |         private=False, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     response = api_client.post(Routes.base, json=registration.dict(by_alias=True)) | ||||||
|  |     assert response.status_code == 201 | ||||||
|  |  | ||||||
|  |     # Login | ||||||
|  |     form_data = {"username": "email@email.com", "password": "fake-password"} | ||||||
|  |  | ||||||
|  |     response = api_client.post(Routes.auth_token, form_data) | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |     token = response.json().get("access_token") | ||||||
|  |  | ||||||
|  |     assert token is not None | ||||||
| @@ -0,0 +1,51 @@ | |||||||
|  | from fastapi.testclient import TestClient | ||||||
|  |  | ||||||
|  | from mealie.schema.group.group_preferences import UpdateGroupPreferences | ||||||
|  | from tests.utils.assertion_helpers import assert_ignore_keys | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Routes: | ||||||
|  |     base = "/api/groups/self" | ||||||
|  |     preferences = "/api/groups/preferences" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_get_preferences(api_client: TestClient, admin_token) -> None: | ||||||
|  |     response = api_client.get(Routes.preferences, headers=admin_token) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |  | ||||||
|  |     preferences = response.json() | ||||||
|  |  | ||||||
|  |     # Spot Check Defaults | ||||||
|  |     assert preferences["recipePublic"] is True | ||||||
|  |     assert preferences["recipeShowNutrition"] is False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_preferences_in_group(api_client: TestClient, admin_token) -> None: | ||||||
|  |     response = api_client.get(Routes.base, headers=admin_token) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |  | ||||||
|  |     group = response.json() | ||||||
|  |  | ||||||
|  |     assert group["preferences"] is not None | ||||||
|  |  | ||||||
|  |     # Spot Check | ||||||
|  |     assert group["preferences"]["recipePublic"] is True | ||||||
|  |     assert group["preferences"]["recipeShowNutrition"] is False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_update_preferences(api_client: TestClient, admin_token) -> None: | ||||||
|  |     new_data = UpdateGroupPreferences(recipe_public=False, recipe_show_nutrition=True) | ||||||
|  |  | ||||||
|  |     response = api_client.put(Routes.preferences, json=new_data.dict(), headers=admin_token) | ||||||
|  |  | ||||||
|  |     assert response.status_code == 200 | ||||||
|  |  | ||||||
|  |     preferences = response.json() | ||||||
|  |  | ||||||
|  |     assert preferences is not None | ||||||
|  |     assert preferences["recipePublic"] is False | ||||||
|  |     assert preferences["recipeShowNutrition"] is True | ||||||
|  |  | ||||||
|  |     assert_ignore_keys(new_data.dict(by_alias=True), preferences, ["id", "groupId"]) | ||||||
| @@ -0,0 +1,71 @@ | |||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from mealie.schema.user.registration import CreateUserRegistration | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_create_user_registration() -> None: | ||||||
|  |     CreateUserRegistration( | ||||||
|  |         group="Home", | ||||||
|  |         group_token=None, | ||||||
|  |         email="SomeValidEmail@email.com", | ||||||
|  |         username="SomeValidUsername", | ||||||
|  |         password="SomeValidPassword", | ||||||
|  |         password_confirm="SomeValidPassword", | ||||||
|  |         advanced=False, | ||||||
|  |         private=True, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     CreateUserRegistration( | ||||||
|  |         group=None, | ||||||
|  |         group_token="asdfadsfasdfasdfasdf", | ||||||
|  |         email="SomeValidEmail@email.com", | ||||||
|  |         username="SomeValidUsername", | ||||||
|  |         password="SomeValidPassword", | ||||||
|  |         password_confirm="SomeValidPassword", | ||||||
|  |         advanced=False, | ||||||
|  |         private=True, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.parametrize("group, group_token", [(None, None), ("", None), (None, "")]) | ||||||
|  | def test_group_or_token_validator(group, group_token) -> None: | ||||||
|  |     with pytest.raises(ValueError): | ||||||
|  |         CreateUserRegistration( | ||||||
|  |             group=group, | ||||||
|  |             group_token=group_token, | ||||||
|  |             email="SomeValidEmail@email.com", | ||||||
|  |             username="SomeValidUsername", | ||||||
|  |             password="SomeValidPassword", | ||||||
|  |             password_confirm="SomeValidPassword", | ||||||
|  |             advanced=False, | ||||||
|  |             private=True, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_group_no_args_passed() -> None: | ||||||
|  |     with pytest.raises(ValueError): | ||||||
|  |         CreateUserRegistration( | ||||||
|  |             email="SomeValidEmail@email.com", | ||||||
|  |             username="SomeValidUsername", | ||||||
|  |             password="SomeValidPassword", | ||||||
|  |             password_confirm="SomeValidPassword", | ||||||
|  |             advanced=False, | ||||||
|  |             private=True, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_password_validator() -> None: | ||||||
|  |     with pytest.raises(ValueError): | ||||||
|  |         CreateUserRegistration( | ||||||
|  |             group=None, | ||||||
|  |             group_token="asdfadsfasdfasdfasdf", | ||||||
|  |             email="SomeValidEmail@email.com", | ||||||
|  |             username="SomeValidUsername", | ||||||
|  |             password="SomeValidPassword", | ||||||
|  |             password_confirm="PasswordDefNotMatch", | ||||||
|  |             advanced=False, | ||||||
|  |             private=True, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | test_create_user_registration() | ||||||
							
								
								
									
										17
									
								
								tests/utils/assertion_helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								tests/utils/assertion_helpers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | def assert_ignore_keys(dict1: dict, dict2: dict, ignore_keys: list) -> None: | ||||||
|  |     """ | ||||||
|  |     Itterates through a list of keys and checks if they are in the the provided ignore_keys list, | ||||||
|  |     if they are not in the ignore_keys list, it checks the value of the key in the provided against | ||||||
|  |     the value provided in dict2. If the value of the key in dict1 is not equal to the value of the | ||||||
|  |     key in dict2, The assertion fails. Useful for testing id / group_id agnostic data | ||||||
|  |  | ||||||
|  |     Note: ignore_keys defaults to ['id', 'group_id'] | ||||||
|  |     """ | ||||||
|  |     if ignore_keys is None: | ||||||
|  |         ignore_keys = ["id", "group_id"] | ||||||
|  |  | ||||||
|  |     for key, value in dict1.items(): | ||||||
|  |         if key in ignore_keys: | ||||||
|  |             continue | ||||||
|  |         else: | ||||||
|  |             assert value == dict2[key] | ||||||
		Reference in New Issue
	
	Block a user