diff --git a/alembic/versions/2024-09-02-21.39.49_be568e39ffdf_added_household_recipe_lock_setting_and_.py b/alembic/versions/2024-09-02-21.39.49_be568e39ffdf_added_household_recipe_lock_setting_and_.py new file mode 100644 index 000000000000..83f2e55186ad --- /dev/null +++ b/alembic/versions/2024-09-02-21.39.49_be568e39ffdf_added_household_recipe_lock_setting_and_.py @@ -0,0 +1,75 @@ +"""added household recipe lock setting and household management user permission + +Revision ID: be568e39ffdf +Revises: feecc8ffb956 +Create Date: 2024-09-02 21:39:49.210355 + +""" + +from textwrap import dedent + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "be568e39ffdf" +down_revision = "feecc8ffb956" +branch_labels: str | tuple[str, ...] | None = None +depends_on: str | tuple[str, ...] | None = None + + +def populate_defaults(): + if op.get_context().dialect.name == "postgresql": + TRUE = "TRUE" + FALSE = "FALSE" + else: + TRUE = "1" + FALSE = "0" + + op.execute( + dedent( + f""" + UPDATE household_preferences + SET lock_recipe_edits_from_other_households = {TRUE} + WHERE lock_recipe_edits_from_other_households IS NULL + """ + ) + ) + op.execute( + dedent( + f""" + UPDATE users + SET can_manage_household = {FALSE} + WHERE can_manage_household IS NULL AND admin = {FALSE} + """ + ) + ) + op.execute( + dedent( + f""" + UPDATE users + SET can_manage_household = {TRUE} + WHERE can_manage_household IS NULL AND admin = {TRUE} + """ + ) + ) + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "household_preferences", + sa.Column("lock_recipe_edits_from_other_households", sa.Boolean(), nullable=True), + ) + op.add_column("users", sa.Column("can_manage_household", sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + populate_defaults() + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("users", "can_manage_household") + op.drop_column("household_preferences", "lock_recipe_edits_from_other_households") + # ### end Alembic commands ### diff --git a/frontend/components/Domain/Household/HouseholdPreferencesEditor.vue b/frontend/components/Domain/Household/HouseholdPreferencesEditor.vue index ab6b8833f5be..e5c8899384db 100644 --- a/frontend/components/Domain/Household/HouseholdPreferencesEditor.vue +++ b/frontend/components/Domain/Household/HouseholdPreferencesEditor.vue @@ -1,7 +1,33 @@ - - + + + + + + {{ $t("household.private-household-description") }} + + + + + + + + + {{ $t("household.lock-recipe-edits-from-other-households-description") }} + + + - - - + + + + + {{ p.description }} + + + - diff --git a/frontend/components/Domain/Recipe/RecipeCard.vue b/frontend/components/Domain/Recipe/RecipeCard.vue index aa22437e3e5c..b6c627c60d3b 100644 --- a/frontend/components/Domain/Recipe/RecipeCard.vue +++ b/frontend/components/Domain/Recipe/RecipeCard.vue @@ -50,7 +50,7 @@ :recipe-id="recipeId" :use-items="{ delete: false, - edit: true, + edit: false, download: true, mealplanner: true, shoppingList: true, diff --git a/frontend/components/Domain/Recipe/RecipeCardMobile.vue b/frontend/components/Domain/Recipe/RecipeCardMobile.vue index 6fa5b8354e8c..3d0641269485 100644 --- a/frontend/components/Domain/Recipe/RecipeCardMobile.vue +++ b/frontend/components/Domain/Recipe/RecipeCardMobile.vue @@ -62,7 +62,7 @@ :recipe-id="recipeId" :use-items="{ delete: false, - edit: true, + edit: false, download: true, mealplanner: true, shoppingList: true, diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue index 18915641ae29..69853821a798 100644 --- a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue +++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue @@ -69,7 +69,8 @@ import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue"; import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue"; import RecipeActionMenu from "~/components/Domain/Recipe/RecipeActionMenu.vue"; import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue"; -import { useStaticRoutes } from "~/composables/api"; +import { useStaticRoutes, useUserApi } from "~/composables/api"; +import { HouseholdSummary } from "~/lib/api/types/household"; import { Recipe } from "~/lib/api/types/recipe"; import { NoUndefinedField } from "~/lib/api/types/non-generated"; import { usePageState, usePageUser, PageMode, EditorMode } from "~/composables/recipe-page/shared-state"; @@ -100,7 +101,15 @@ export default defineComponent({ const { imageKey, pageMode, editMode, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug); const { user } = usePageUser(); const { isOwnGroup } = useLoggedInState(); - const { canEditRecipe } = useRecipePermissions(props.recipe, user); + + const recipeHousehold = ref(); + if (user) { + const userApi = useUserApi(); + userApi.groups.fetchHousehold(props.recipe.householdId).then(({ data }) => { + recipeHousehold.value = data || undefined; + }); + } + const { canEditRecipe } = useRecipePermissions(props.recipe, recipeHousehold, user); function printRecipe() { window.print(); diff --git a/frontend/composables/recipes/use-recipe-permissions.test.ts b/frontend/composables/recipes/use-recipe-permissions.test.ts index 6dedeb32b378..a962d7af34d2 100644 --- a/frontend/composables/recipes/use-recipe-permissions.test.ts +++ b/frontend/composables/recipes/use-recipe-permissions.test.ts @@ -1,5 +1,7 @@ import { describe, test, expect } from "vitest"; +import { ref, Ref } from "@nuxtjs/composition-api"; import { useRecipePermissions } from "./use-recipe-permissions"; +import { HouseholdSummary } from "~/lib/api/types/household"; import { Recipe } from "~/lib/api/types/recipe"; import { UserOut } from "~/lib/api/types/user"; @@ -32,35 +34,76 @@ describe("test use recipe permissions", () => { ...overrides, }); + const createRecipeHousehold = (overrides: Partial, lockRecipeEdits = false): Ref => ( + ref({ + id: commonHouseholdId, + groupId: commonGroupId, + name: "My Household", + slug: "my-household", + preferences: { + id: "my-household-preferences-id", + lockRecipeEditsFromOtherHouseholds: lockRecipeEdits, + }, + ...overrides, + }) + ); + test("when user is null, cannot edit", () => { - const result = useRecipePermissions(createRecipe({}), null); + const result = useRecipePermissions(createRecipe({}), createRecipeHousehold({}), null); expect(result.canEditRecipe.value).toBe(false); }); test("when user is recipe owner, can edit", () => { - const result = useRecipePermissions(createRecipe({}), createUser({})); + const result = useRecipePermissions(createRecipe({}), ref(), createUser({})); expect(result.canEditRecipe.value).toBe(true); }); - test("when user is not recipe owner, is correct group and household, and recipe is unlocked, can edit", () => { - const result = useRecipePermissions( - createRecipe({}), - createUser({ id: "other-user-id" }), - ); - expect(result.canEditRecipe.value).toBe(true); - }); + test( + "when user is not recipe owner, is correct group and household, recipe is unlocked, and household is unlocked, can edit", + () => { + const result = useRecipePermissions( + createRecipe({}), + createRecipeHousehold({}), + createUser({ id: "other-user-id" }), + ); + expect(result.canEditRecipe.value).toBe(true); + } + ); + + test( + "when user is not recipe owner, is correct group and household, recipe is unlocked, but household is locked, can edit", + () => { + const result = useRecipePermissions( + createRecipe({}), + createRecipeHousehold({}, true), + createUser({ id: "other-user-id" }), + ); + expect(result.canEditRecipe.value).toBe(true); + } + ); test("when user is not recipe owner, and user is other group, cannot edit", () => { const result = useRecipePermissions( createRecipe({}), + createRecipeHousehold({}), createUser({ id: "other-user-id", groupId: "other-group-id"}), ); expect(result.canEditRecipe.value).toBe(false); }); - test("when user is not recipe owner, and user is other household, cannot edit", () => { + test("when user is not recipe owner, and user is other household, and household is unlocked, can edit", () => { const result = useRecipePermissions( createRecipe({}), + createRecipeHousehold({}), + createUser({ id: "other-user-id", householdId: "other-household-id" }), + ); + expect(result.canEditRecipe.value).toBe(true); + }); + + test("when user is not recipe owner, and user is other household, and household is locked, cannot edit", () => { + const result = useRecipePermissions( + createRecipe({}), + createRecipeHousehold({}, true), createUser({ id: "other-user-id", householdId: "other-household-id" }), ); expect(result.canEditRecipe.value).toBe(false); @@ -69,13 +112,14 @@ describe("test use recipe permissions", () => { test("when user is not recipe owner, and recipe is locked, cannot edit", () => { const result = useRecipePermissions( createRecipe({}, true), + createRecipeHousehold({}), createUser({ id: "other-user-id"}), ); expect(result.canEditRecipe.value).toBe(false); }); - test("when user is recipe owner, and recipe is locked, can edit", () => { - const result = useRecipePermissions(createRecipe({}, true), createUser({})); + test("when user is recipe owner, and recipe is locked, and household is locked, can edit", () => { + const result = useRecipePermissions(createRecipe({}, true), createRecipeHousehold({}, true), createUser({})); expect(result.canEditRecipe.value).toBe(true); }); }); diff --git a/frontend/composables/recipes/use-recipe-permissions.ts b/frontend/composables/recipes/use-recipe-permissions.ts index bd3af98dca1c..d4efbfd058b5 100644 --- a/frontend/composables/recipes/use-recipe-permissions.ts +++ b/frontend/composables/recipes/use-recipe-permissions.ts @@ -1,34 +1,44 @@ -import { computed } from "@nuxtjs/composition-api"; +import { computed, Ref } from "@nuxtjs/composition-api"; import { Recipe } from "~/lib/api/types/recipe"; +import { HouseholdSummary } from "~/lib/api/types/household"; import { UserOut } from "~/lib/api/types/user"; -export function useRecipePermissions(recipe: Recipe, user: UserOut | null) { - const canEditRecipe = computed(() => { - // Check recipe owner - if (!user?.id) { - return false; - } - if (user.id === recipe.userId) { - return true; - } - - // Check group and household - if (user.groupId !== recipe.groupId) { - return false; - } - if (user.householdId !== recipe.householdId) { - return false; - } - - // Check recipe - if (recipe.settings?.locked) { - return false; - } - - return true; - }); - - return { - canEditRecipe, +export function useRecipePermissions( + recipe: Recipe, + recipeHousehold: Ref, + user: UserOut | null, +) { + const canEditRecipe = computed(() => { + // Check recipe owner + if (!user?.id) { + return false; } + if (user.id === recipe.userId) { + return true; + } + + // Check group and household + if (user.groupId !== recipe.groupId) { + return false; + } + if (user.householdId !== recipe.householdId) { + if (!recipeHousehold.value?.preferences) { + return false; + } + if (recipeHousehold.value?.preferences.lockRecipeEditsFromOtherHouseholds) { + return false; + } + } + + // Check recipe + if (recipe.settings?.locked) { + return false; + } + + return true; + }); + + return { + canEditRecipe, + } } diff --git a/frontend/composables/use-households.ts b/frontend/composables/use-households.ts index 618ab75636f3..44f9caf8f4ec 100644 --- a/frontend/composables/use-households.ts +++ b/frontend/composables/use-households.ts @@ -36,6 +36,8 @@ export const useHouseholdSelf = function () { if (data) { householdSelfRef.value.preferences = data; } + + return data || undefined; }, }; diff --git a/frontend/composables/use-users/user-form.ts b/frontend/composables/use-users/user-form.ts index 387f56f49c47..79b889f3f0f4 100644 --- a/frontend/composables/use-users/user-form.ts +++ b/frontend/composables/use-users/user-form.ts @@ -65,6 +65,12 @@ export const useUserForm = () => { type: fieldTypes.BOOLEAN, rules: ["required"], }, + { + label: i18n.tc("user.user-can-manage-household"), + varName: "canManageHousehold", + type: fieldTypes.BOOLEAN, + rules: ["required"], + }, { label: i18n.tc("user.enable-advanced-features"), varName: "advanced", diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json index 0ee1fadc3160..382aa53aca0a 100644 --- a/frontend/lang/messages/en-US.json +++ b/frontend/lang/messages/en-US.json @@ -241,13 +241,14 @@ "manage-members": "Manage Members", "manage-members-description": "Manage the permissions of the members in your household. {manage} allows the user to access the data-management page, and {invite} allows the user to generate invitation links for other users. Group owners cannot change their own permissions.", "manage": "Manage", + "manage-household": "Manage Household", "invite": "Invite", "looking-to-update-your-profile": "Looking to Update Your Profile?", "default-recipe-preferences-description": "These are the default settings when a new recipe is created in your group. These can be changed for individual recipes in the recipe settings menu.", "default-recipe-preferences": "Default Recipe Preferences", "group-preferences": "Group Preferences", "private-group": "Private Group", - "private-group-description": "Setting your group to private will default all public view options to default. This overrides any individual households or recipes public view settings.", + "private-group-description": "Setting your group to private will disable all public view options. This overrides any individual public view settings", "enable-public-access": "Enable Public Access", "enable-public-access-description": "Make group recipes public by default, and allow visitors to view recipes without logging-in", "allow-users-outside-of-your-group-to-see-your-recipes": "Allow users outside of your group to see your recipes", @@ -285,7 +286,9 @@ "admin-household-management-text": "Changes to this household will be reflected immediately.", "household-id-value": "Household Id: {0}", "private-household": "Private Household", - "private-household-description": "Setting your household to private will default all public view options to default. This overrides any individual recipes public view settings.", + "private-household-description": "Setting your household to private will disable all public view options. This overrides any individual public view settings", + "lock-recipe-edits-from-other-households": "Lock recipe edits from other households", + "lock-recipe-edits-from-other-households-description": "When enabled only users in your household can edit recipes created by your household", "household-recipe-preferences": "Household Recipe Preferences", "default-recipe-preferences-description": "These are the default settings when a new recipe is created in your household. These can be changed for individual recipes in the recipe settings menu.", "allow-users-outside-of-your-household-to-see-your-recipes": "Allow users outside of your household to see your recipes", @@ -987,6 +990,7 @@ "administrator": "Administrator", "user-can-invite-other-to-group": "User can invite others to group", "user-can-manage-group": "User can manage group", + "user-can-manage-household": "User can manage household", "user-can-organize-group-data": "User can organize group data", "enable-advanced-features": "Enable advanced features", "it-looks-like-this-is-your-first-time-logging-in": "It looks like this is your first time logging in.", diff --git a/frontend/lib/api/types/admin.ts b/frontend/lib/api/types/admin.ts index 88bf0f22f213..6f42b40d4431 100644 --- a/frontend/lib/api/types/admin.ts +++ b/frontend/lib/api/types/admin.ts @@ -16,6 +16,7 @@ export interface AdminAboutInfo { oidcRedirect: boolean; oidcProviderName: string; enableOpenai: boolean; + enableOpenaiImageServices: boolean; versionLatest: string; apiPort: number; apiDocs: boolean; diff --git a/frontend/lib/api/types/household.ts b/frontend/lib/api/types/household.ts index 7841bd3861f3..79b1a7ffaf5a 100644 --- a/frontend/lib/api/types/household.ts +++ b/frontend/lib/api/types/household.ts @@ -15,6 +15,7 @@ export interface CreateGroupRecipeAction { } export interface CreateHouseholdPreferences { privateHousehold?: boolean; + lockRecipeEditsFromOtherHouseholds?: boolean; firstDayOfWeek?: number; recipePublic?: boolean; recipeShowNutrition?: boolean; @@ -185,6 +186,7 @@ export interface HouseholdInDB { } export interface ReadHouseholdPreferences { privateHousehold?: boolean; + lockRecipeEditsFromOtherHouseholds?: boolean; firstDayOfWeek?: number; recipePublic?: boolean; recipeShowNutrition?: boolean; @@ -241,6 +243,7 @@ export interface SaveGroupRecipeAction { } export interface SaveHouseholdPreferences { privateHousehold?: boolean; + lockRecipeEditsFromOtherHouseholds?: boolean; firstDayOfWeek?: number; recipePublic?: boolean; recipeShowNutrition?: boolean; @@ -267,6 +270,7 @@ export interface SaveWebhook { } export interface SetPermissions { userId: string; + canManageHousehold?: boolean; canManage?: boolean; canInvite?: boolean; canOrganize?: boolean; @@ -649,6 +653,7 @@ export interface UpdateHouseholdAdmin { } export interface UpdateHouseholdPreferences { privateHousehold?: boolean; + lockRecipeEditsFromOtherHouseholds?: boolean; firstDayOfWeek?: number; recipePublic?: boolean; recipeShowNutrition?: boolean; diff --git a/frontend/lib/api/types/openai.ts b/frontend/lib/api/types/openai.ts index ac4a29bd9988..f1a358ae084f 100644 --- a/frontend/lib/api/types/openai.ts +++ b/frontend/lib/api/types/openai.ts @@ -62,4 +62,159 @@ export interface OpenAIIngredient { export interface OpenAIIngredients { ingredients?: OpenAIIngredient[]; } +export interface OpenAIRecipe { + /** + * + * The name or title of the recipe. If you're unable to determine the name of the recipe, you should + * make your best guess based upon the ingredients and instructions provided. + * + */ + name: string; + /** + * + * A long description of the recipe. This should be a string that describes the recipe in a few words + * or sentences. If the recipe doesn't have a description, you should return None. + * + */ + description: string | null; + /** + * + * The yield of the recipe. For instance, if the recipe makes 12 cookies, the yield is "12 cookies". + * If the recipe makes 2 servings, the yield is "2 servings". Typically yield consists of a number followed + * by the word "serving" or "servings", but it can be any string that describes the yield. If the yield + * isn't specified, you should return None. + * + */ + recipe_yield?: string | null; + /** + * + * The total time it takes to make the recipe. This should be a string that describes a duration of time, + * such as "1 hour and 30 minutes", "90 minutes", or "1.5 hours". If the recipe has multiple times, choose + * the longest time. If the recipe doesn't specify a total time or duration, or it specifies a prep time or + * perform time but not a total time, you should return None. Do not duplicate times between total time, prep + * time and perform time. + * + */ + total_time?: string | null; + /** + * + * The time it takes to prepare the recipe. This should be a string that describes a duration of time, + * such as "30 minutes", "1 hour", or "1.5 hours". If the recipe has a total time, the prep time should be + * less than the total time. If the recipe doesn't specify a prep time, you should return None. If the recipe + * supplies only one time, it should be the total time. Do not duplicate times between total time, prep + * time and coperformok time. + * + */ + prep_time?: string | null; + /** + * + * The time it takes to cook the recipe. This should be a string that describes a duration of time, + * such as "30 minutes", "1 hour", or "1.5 hours". If the recipe has a total time, the perform time should be + * less than the total time. If the recipe doesn't specify a perform time, you should return None. If the + * recipe specifies a cook time, active time, or other time besides total or prep, you should use that + * time as the perform time. If the recipe supplies only one time, it should be the total time, and not the + * perform time. Do not duplicate times between total time, prep time and perform time. + * + */ + perform_time?: string | null; + /** + * + * A list of ingredients used in the recipe. Ingredients should be inserted in the order they appear in the + * recipe. If the recipe has no ingredients, you should return an empty list. + * + * Often times, but not always, ingredients are separated by line breaks. Use these as a guide to + * separate ingredients. + * + */ + ingredients?: OpenAIRecipeIngredient[]; + /** + * + * A list of ingredients used in the recipe. Ingredients should be inserted in the order they appear in the + * recipe. If the recipe has no ingredients, you should return an empty list. + * + * Often times, but not always, instructions are separated by line breaks and/or separated by paragraphs. + * Use these as a guide to separate instructions. They also may be separated by numbers or words, such as + * "1.", "2.", "Step 1", "Step 2", "First", "Second", etc. + * + */ + instructions?: OpenAIRecipeInstruction[]; + /** + * + * A list of notes found in the recipe. Notes should be inserted in the order they appear in the recipe. + * They may appear anywhere on the recipe, though they are typically found under the instructions. + * + */ + notes?: OpenAIRecipeNotes[]; +} +export interface OpenAIRecipeIngredient { + /** + * + * The title of the section of the recipe that the ingredient is found in. Recipes may not specify + * ingredient sections, in which case this should be left blank. + * Only the first item in the section should have this set, + * whereas subsuquent items should have their titles left blank (unless they start a new section). + * + */ + title?: string | null; + /** + * + * The text of the ingredient. This should represent the entire ingredient, such as "1 cup of flour" or + * "2 cups of onions, chopped". If the ingredient is completely blank, skip it and do not add the ingredient, + * since this field is required. + * + * If the ingredient has no text, but has a title, include the title on the + * next ingredient instead. + * + */ + text: string; +} +export interface OpenAIRecipeInstruction { + /** + * + * The title of the section of the recipe that the instruction is found in. Recipes may not specify + * instruction sections, in which case this should be left blank. + * Only the first instruction in the section should have this set, + * whereas subsuquent instructions should have their titles left blank (unless they start a new section). + * + */ + title?: string | null; + /** + * + * The text of the instruction. This represents one step in the recipe, such as "Preheat the oven to 350", + * or "Sauté the onions for 20 minutes". Sometimes steps can be longer, such as "Bring a large pot of lightly + * salted water to a boil. Add ditalini pasta and cook for 8 minutes or until al dente; drain.". + * + * Sometimes, but not always, recipes will include their number in front of the text, such as + * "1.", "2.", or "Step 1", "Step 2", or "First", "Second". In the case where they are directly numbered + * ("1.", "2.", "Step one", "Step 1", "Step two", "Step 2", etc.), you should not include the number in + * the text. However, if they use words ("First", "Second", etc.), then those should be included. + * + * If the instruction is completely blank, skip it and do not add the instruction, since this field is + * required. If the ingredient has no text, but has a title, include the title on the next + * instruction instead. + * + */ + text: string; +} +export interface OpenAIRecipeNotes { + /** + * + * The title of the note. Notes may not specify a title, and just have a body of text. In this case, + * title should be left blank, and all content should go in the note text. If the note title is just + * "note" or "info", you should ignore it and leave the title blank. + * + */ + title?: string | null; + /** + * + * The text of the note. This should represent the entire note, such as "This recipe is great for + * a summer picnic" or "This recipe is a family favorite". They may also include additional prep + * instructions such as "to make this recipe gluten free, use gluten free flour", or "you may prepare + * the dough the night before and refrigerate it until ready to bake". + * + * If the note is completely blank, skip it and do not add the note, since this field is required. + * + */ + text: string; +} export interface OpenAIBase {} diff --git a/frontend/lib/api/types/user.ts b/frontend/lib/api/types/user.ts index 8b2218f0015d..81fd6a7014c4 100644 --- a/frontend/lib/api/types/user.ts +++ b/frontend/lib/api/types/user.ts @@ -114,6 +114,7 @@ export interface PrivateUser { advanced?: boolean; canInvite?: boolean; canManage?: boolean; + canManageHousehold?: boolean; canOrganize?: boolean; groupId: string; groupSlug: string; @@ -189,6 +190,7 @@ export interface UserBase { advanced?: boolean; canInvite?: boolean; canManage?: boolean; + canManageHousehold?: boolean; canOrganize?: boolean; } export interface UserIn { @@ -203,6 +205,7 @@ export interface UserIn { advanced?: boolean; canInvite?: boolean; canManage?: boolean; + canManageHousehold?: boolean; canOrganize?: boolean; password: string; } @@ -218,6 +221,7 @@ export interface UserOut { advanced?: boolean; canInvite?: boolean; canManage?: boolean; + canManageHousehold?: boolean; canOrganize?: boolean; groupId: string; groupSlug: string; diff --git a/frontend/lib/api/user/groups.ts b/frontend/lib/api/user/groups.ts index a43063988ce6..5dc85110abbe 100644 --- a/frontend/lib/api/user/groups.ts +++ b/frontend/lib/api/user/groups.ts @@ -15,7 +15,8 @@ const routes = { groupsSelf: `${prefix}/groups/self`, preferences: `${prefix}/groups/preferences`, storage: `${prefix}/groups/storage`, - households: `${prefix}/households`, + households: `${prefix}/groups/households`, + householdsId: (id: string | number) => `${prefix}/groups/households/${id}`, membersHouseholdId: (householdId: string | number | null) => { return householdId ? `${prefix}/households/members?householdId=${householdId}` : @@ -50,6 +51,10 @@ export class GroupAPI extends BaseCRUDAPI(routes.households); } + async fetchHousehold(householdId: string | number) { + return await this.requests.get(routes.householdsId(householdId)); + } + async storage() { return await this.requests.get(routes.storage); } diff --git a/frontend/middleware/can-manage-household-only.ts b/frontend/middleware/can-manage-household-only.ts new file mode 100644 index 000000000000..0e4509e9e86c --- /dev/null +++ b/frontend/middleware/can-manage-household-only.ts @@ -0,0 +1,11 @@ +interface CanManageRedirectParams { + $auth: any + redirect: (path: string) => void +} +export default function ({ $auth, redirect }: CanManageRedirectParams) { + // If the user is not allowed to manage group settings redirect to the home page + if (!$auth.user?.canManageHousehold) { + console.warn("User is not allowed to manage household settings"); + return redirect("/"); + } +} diff --git a/frontend/pages/admin/manage/households/_id.vue b/frontend/pages/admin/manage/households/_id.vue index 174ab7c1b10b..3e897cff2b1a 100644 --- a/frontend/pages/admin/manage/households/_id.vue +++ b/frontend/pages/admin/manage/households/_id.vue @@ -53,7 +53,7 @@ import { VForm } from "~/types/vuetify"; export default defineComponent({ components: { - HouseholdPreferencesEditor, + HouseholdPreferencesEditor, }, layout: "admin", setup() { @@ -94,11 +94,8 @@ export default defineComponent({ const { response, data } = await userApi.households.updateOne(household.value.id, household.value); if (response?.status === 200 && data) { - if (household.value.slug !== data.slug) { - // the slug updated, which invalidates the nav URLs - window.location.reload(); - } household.value = data; + alert.success(i18n.tc("settings.settings-updated")); } else { alert.error(i18n.tc("settings.settings-update-failed")); } diff --git a/frontend/pages/household/index.vue b/frontend/pages/household/index.vue index 3e40b173ed6e..0416a2bbad3a 100644 --- a/frontend/pages/household/index.vue +++ b/frontend/pages/household/index.vue @@ -1,5 +1,5 @@ - + @@ -7,70 +7,38 @@ {{ $t("profile.household-settings") }} {{ $t("profile.household-description") }} - - - - - - - - {{ $t("household.private-household-description") }} - - - + + + + + + + + {{ $t("general.update") }} - - - - - - {{ $t("household.default-recipe-preferences-description") }} - - - - - - - {{ p.description }} - - - - +
+ {{ $t("household.private-household-description") }} +
+ {{ $t("household.lock-recipe-edits-from-other-households-description") }} +
+ {{ p.description }} +
- {{ $t("household.private-household-description") }} -
- {{ p.description }} -