mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -04:00 
			
		
		
		
	Feature/improve user feedback (#1076)
* add proper type annotations * fix state management and dead code * add response messages
This commit is contained in:
		| @@ -50,7 +50,7 @@ | |||||||
|               </v-card> |               </v-card> | ||||||
|             </div> |             </div> | ||||||
|             <div v-if="state" key="change-password"> |             <div v-if="state" key="change-password"> | ||||||
|               <BaseCardSectionTitle class="mt-10" :title="$t('settings.change-password')"> </BaseCardSectionTitle> |               <BaseCardSectionTitle class="mt-10" :title="$tc('settings.change-password')"> </BaseCardSectionTitle> | ||||||
|               <v-card outlined> |               <v-card outlined> | ||||||
|                 <v-card-text class="pb-0"> |                 <v-card-text class="pb-0"> | ||||||
|                   <v-form ref="passChange"> |                   <v-form ref="passChange"> | ||||||
| @@ -60,14 +60,16 @@ | |||||||
|                       :label="$t('user.current-password')" |                       :label="$t('user.current-password')" | ||||||
|                       validate-on-blur |                       validate-on-blur | ||||||
|                       :type="showPassword ? 'text' : 'password'" |                       :type="showPassword ? 'text' : 'password'" | ||||||
|                       @click:append="showPassword.current = !showPassword.current" |                       :append-icon="showPassword ? $globals.icons.eye : $globals.icons.eyeOff" | ||||||
|  |                       @click:append="showPassword = !showPassword" | ||||||
|                     ></v-text-field> |                     ></v-text-field> | ||||||
|                     <v-text-field |                     <v-text-field | ||||||
|                       v-model="password.newOne" |                       v-model="password.newOne" | ||||||
|                       :prepend-icon="$globals.icons.lock" |                       :prepend-icon="$globals.icons.lock" | ||||||
|                       :label="$t('user.new-password')" |                       :label="$t('user.new-password')" | ||||||
|                       :type="showPassword ? 'text' : 'password'" |                       :type="showPassword ? 'text' : 'password'" | ||||||
|                       @click:append="showPassword.newOne = !showPassword.newOne" |                       :append-icon="showPassword ? $globals.icons.eye : $globals.icons.eyeOff" | ||||||
|  |                       @click:append="showPassword = !showPassword" | ||||||
|                     ></v-text-field> |                     ></v-text-field> | ||||||
|                     <v-text-field |                     <v-text-field | ||||||
|                       v-model="password.newTwo" |                       v-model="password.newTwo" | ||||||
| @@ -76,13 +78,18 @@ | |||||||
|                       :rules="[password.newOne === password.newTwo || $t('user.password-must-match')]" |                       :rules="[password.newOne === password.newTwo || $t('user.password-must-match')]" | ||||||
|                       validate-on-blur |                       validate-on-blur | ||||||
|                       :type="showPassword ? 'text' : 'password'" |                       :type="showPassword ? 'text' : 'password'" | ||||||
|                       @click:append="showPassword.newTwo = !showPassword.newTwo" |                       :append-icon="showPassword ? $globals.icons.eye : $globals.icons.eyeOff" | ||||||
|  |                       @click:append="showPassword = !showPassword" | ||||||
|                     ></v-text-field> |                     ></v-text-field> | ||||||
|                   </v-form> |                   </v-form> | ||||||
|                 </v-card-text> |                 </v-card-text> | ||||||
|                 <v-card-actions> |                 <v-card-actions> | ||||||
|                   <v-spacer></v-spacer> |                   <v-spacer></v-spacer> | ||||||
|                   <BaseButton update @click="updatePassword" /> |                   <BaseButton | ||||||
|  |                     update | ||||||
|  |                     :disabled="!passwordsMatch || password.current.length < 0" | ||||||
|  |                     @click="updatePassword" | ||||||
|  |                   /> | ||||||
|                 </v-card-actions> |                 </v-card-actions> | ||||||
|               </v-card> |               </v-card> | ||||||
|             </div> |             </div> | ||||||
| @@ -112,7 +119,7 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { ref, reactive, defineComponent, computed, useContext, watch } from "@nuxtjs/composition-api"; | import { ref, reactive, defineComponent, computed, useContext, watch, toRefs } from "@nuxtjs/composition-api"; | ||||||
| import { useUserApi } from "~/composables/api"; | import { useUserApi } from "~/composables/api"; | ||||||
| import UserAvatar from "~/components/Domain/User/UserAvatar.vue"; | import UserAvatar from "~/components/Domain/User/UserAvatar.vue"; | ||||||
| import { VForm } from "~/types/vuetify"; | import { VForm } from "~/types/vuetify"; | ||||||
| @@ -141,6 +148,8 @@ export default defineComponent({ | |||||||
|       newTwo: "", |       newTwo: "", | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     const passwordsMatch = computed(() => password.newOne === password.newTwo && password.newOne.length > 0); | ||||||
|  |  | ||||||
|     async function updateUser() { |     async function updateUser() { | ||||||
|       const { response } = await api.users.updateOne(userCopy.value.id, userCopy.value); |       const { response } = await api.users.updateOne(userCopy.value.id, userCopy.value); | ||||||
|       if (response?.status === 200) { |       if (response?.status === 200) { | ||||||
| @@ -162,15 +171,14 @@ export default defineComponent({ | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return { updateUser, updatePassword, userCopy, password, domUpdatePassword }; |     const state = reactive({ | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       hideImage: false, |       hideImage: false, | ||||||
|       passwordLoading: false, |       passwordLoading: false, | ||||||
|       showPassword: false, |       showPassword: false, | ||||||
|       loading: false, |       loading: false, | ||||||
|     }; |     }); | ||||||
|  |  | ||||||
|  |     return { ...toRefs(state), updateUser, updatePassword, userCopy, password, domUpdatePassword, passwordsMatch }; | ||||||
|   }, |   }, | ||||||
|   head() { |   head() { | ||||||
|     return { |     return { | ||||||
| @@ -179,4 +187,3 @@ export default defineComponent({ | |||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,11 +2,13 @@ from abc import ABC | |||||||
| from functools import cached_property | from functools import cached_property | ||||||
|  |  | ||||||
| from fastapi import Depends | from fastapi import Depends | ||||||
|  | from pydantic import UUID4 | ||||||
|  |  | ||||||
| from mealie.core.exceptions import mealie_registered_exceptions | from mealie.core.exceptions import mealie_registered_exceptions | ||||||
| from mealie.repos.all_repositories import AllRepositories | from mealie.repos.all_repositories import AllRepositories | ||||||
| from mealie.routes._base.checks import OperationChecks | from mealie.routes._base.checks import OperationChecks | ||||||
| from mealie.routes._base.dependencies import SharedDependencies | from mealie.routes._base.dependencies import SharedDependencies | ||||||
|  | from mealie.schema.user.user import GroupInDB, PrivateUser | ||||||
|  |  | ||||||
|  |  | ||||||
| class BasePublicController(ABC): | class BasePublicController(ABC): | ||||||
| @@ -39,15 +41,15 @@ class BaseUserController(ABC): | |||||||
|         return AllRepositories(self.deps.session) |         return AllRepositories(self.deps.session) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def group_id(self): |     def group_id(self) -> UUID4: | ||||||
|         return self.deps.acting_user.group_id |         return self.deps.acting_user.group_id | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def user(self): |     def user(self) -> PrivateUser: | ||||||
|         return self.deps.acting_user |         return self.deps.acting_user | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def group(self): |     def group(self) -> GroupInDB: | ||||||
|         return self.deps.repos.groups.get_one(self.group_id) |         return self.deps.repos.groups.get_one(self.group_id) | ||||||
|  |  | ||||||
|     @cached_property |     @cached_property | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| from fastapi import HTTPException, status | from fastapi import HTTPException, status | ||||||
|  | from pydantic import UUID4 | ||||||
|  |  | ||||||
| from mealie.schema.user.user import PrivateUser | from mealie.schema.user.user import PrivateUser | ||||||
|  |  | ||||||
|  |  | ||||||
| def assert_user_change_allowed(id: int, current_user: PrivateUser): | def assert_user_change_allowed(id: UUID4, current_user: PrivateUser): | ||||||
|     if current_user.id != id and not current_user.admin: |     if current_user.id != id and not current_user.admin: | ||||||
|         # only admins can edit other users |         # only admins can edit other users | ||||||
|         raise HTTPException(status.HTTP_403_FORBIDDEN, detail="NOT_AN_ADMIN") |         raise HTTPException(status.HTTP_403_FORBIDDEN, detail="NOT_AN_ADMIN") | ||||||
|   | |||||||
| @@ -1,13 +1,13 @@ | |||||||
| from fastapi import HTTPException, status | from fastapi import HTTPException, status | ||||||
| from pydantic import UUID4 | from pydantic import UUID4 | ||||||
|  |  | ||||||
| from mealie.core import security |  | ||||||
| from mealie.core.security import hash_password, verify_password | from mealie.core.security import hash_password, verify_password | ||||||
| from mealie.routes._base import BaseAdminController, controller | from mealie.routes._base import BaseAdminController, controller | ||||||
| from mealie.routes._base.abc_controller import BaseUserController | from mealie.routes._base.abc_controller import BaseUserController | ||||||
| from mealie.routes._base.mixins import CrudMixins | from mealie.routes._base.mixins import CrudMixins | ||||||
| from mealie.routes._base.routers import AdminAPIRouter, UserAPIRouter | from mealie.routes._base.routers import AdminAPIRouter, UserAPIRouter | ||||||
| from mealie.routes.users._helpers import assert_user_change_allowed | from mealie.routes.users._helpers import assert_user_change_allowed | ||||||
|  | from mealie.schema.response import ErrorResponse, SuccessResponse | ||||||
| from mealie.schema.user import ChangePassword, UserBase, UserIn, UserOut | from mealie.schema.user import ChangePassword, UserBase, UserIn, UserOut | ||||||
|  |  | ||||||
| user_router = UserAPIRouter(prefix="/users", tags=["Users: CRUD"]) | user_router = UserAPIRouter(prefix="/users", tags=["Users: CRUD"]) | ||||||
| @@ -57,23 +57,39 @@ class UserController(BaseUserController): | |||||||
|  |  | ||||||
|         if not self.user.admin and (new_data.admin or self.user.group != new_data.group): |         if not self.user.admin and (new_data.admin or self.user.group != new_data.group): | ||||||
|             # prevent a regular user from doing admin tasks on themself |             # prevent a regular user from doing admin tasks on themself | ||||||
|             raise HTTPException(status.HTTP_403_FORBIDDEN) |             raise HTTPException( | ||||||
|  |                 status.HTTP_403_FORBIDDEN, ErrorResponse.respond("User doesn't have permission to change group") | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         if self.user.id == item_id and self.user.admin and not new_data.admin: |         if self.user.id == item_id and self.user.admin and not new_data.admin: | ||||||
|             # prevent an admin from demoting themself |             # prevent an admin from demoting themself | ||||||
|             raise HTTPException(status.HTTP_403_FORBIDDEN) |             raise HTTPException( | ||||||
|  |                 status.HTTP_403_FORBIDDEN, ErrorResponse.respond("User doesn't have permission to change group") | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         try: | ||||||
|             self.repos.users.update(item_id, new_data.dict()) |             self.repos.users.update(item_id, new_data.dict()) | ||||||
|  |         except Exception as e: | ||||||
|  |             raise HTTPException( | ||||||
|  |                 status.HTTP_400_BAD_REQUEST, | ||||||
|  |                 ErrorResponse.respond("Failed to update user"), | ||||||
|  |             ) from e | ||||||
|  |  | ||||||
|         if self.user.id == item_id: |         return SuccessResponse.respond("User updated") | ||||||
|             access_token = security.create_access_token(data=dict(sub=str(self.user.id))) |  | ||||||
|             return {"access_token": access_token, "token_type": "bearer"} |  | ||||||
|  |  | ||||||
|     @user_router.put("/{item_id}/password") |     @user_router.put("/{item_id}/password") | ||||||
|     def update_password(self, password_change: ChangePassword): |     def update_password(self, password_change: ChangePassword): | ||||||
|         """Resets the User Password""" |         """Resets the User Password""" | ||||||
|         if not verify_password(password_change.current_password, self.user.password): |         if not verify_password(password_change.current_password, self.user.password): | ||||||
|             raise HTTPException(status.HTTP_400_BAD_REQUEST) |             raise HTTPException(status.HTTP_400_BAD_REQUEST, ErrorResponse.respond("Invalid current password")) | ||||||
|  |  | ||||||
|         self.user.password = hash_password(password_change.new_password) |         self.user.password = hash_password(password_change.new_password) | ||||||
|         return self.repos.users.update_password(self.user.id, self.user.password) |         try: | ||||||
|  |             self.repos.users.update_password(self.user.id, self.user.password) | ||||||
|  |         except Exception as e: | ||||||
|  |             raise HTTPException( | ||||||
|  |                 status.HTTP_400_BAD_REQUEST, | ||||||
|  |                 ErrorResponse.respond("Failed to update password"), | ||||||
|  |             ) from e | ||||||
|  |  | ||||||
|  |         return SuccessResponse.respond("Password updated") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user