diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json index 36a435d377f6..d52a11988b07 100644 --- a/frontend/lang/messages/en-US.json +++ b/frontend/lang/messages/en-US.json @@ -689,6 +689,7 @@ "error-cannot-delete-super-user": "Error! Cannot Delete Super User", "existing-password-does-not-match": "Existing password does not match", "full-name": "Full Name", + "generate-password-reset-link": "Generate Password Reset Link", "invite-only": "Invite Only", "link-id": "Link ID", "link-name": "Link Name", diff --git a/frontend/lib/api/admin/admin-users.ts b/frontend/lib/api/admin/admin-users.ts index bfae183bcbe3..478d7c642ecf 100644 --- a/frontend/lib/api/admin/admin-users.ts +++ b/frontend/lib/api/admin/admin-users.ts @@ -1,5 +1,5 @@ import { BaseCRUDAPI } from "../base/base-clients"; -import { UnlockResults, UserIn, UserOut } from "~/lib/api/types/user"; +import { ForgotPassword, PasswordResetToken, UnlockResults, UserIn, UserOut } from "~/lib/api/types/user"; const prefix = "/api"; @@ -7,6 +7,7 @@ const routes = { adminUsers: `${prefix}/admin/users`, adminUsersId: (tag: string) => `${prefix}/admin/users/${tag}`, adminResetLockedUsers: (force: boolean) => `${prefix}/admin/users/unlock?force=${force ? "true" : "false"}`, + adminPasswordResetToken: `${prefix}/admin/users/password-reset-token`, }; export class AdminUsersApi extends BaseCRUDAPI { @@ -16,4 +17,8 @@ export class AdminUsersApi extends BaseCRUDAPI { async unlockAllUsers(force = false) { return await this.requests.post(routes.adminResetLockedUsers(force), {}); } + + async generatePasswordResetToken(payload: ForgotPassword) { + return await this.requests.post(routes.adminPasswordResetToken, payload); + } } diff --git a/frontend/lib/api/types/user.ts b/frontend/lib/api/types/user.ts index 5818b049b7f7..26e607beb58c 100644 --- a/frontend/lib/api/types/user.ts +++ b/frontend/lib/api/types/user.ts @@ -233,3 +233,6 @@ export interface UserIn { export interface ValidateResetToken { token: string; } +export interface PasswordResetToken { + token: string; +} diff --git a/frontend/pages/admin/manage/users/_id.vue b/frontend/pages/admin/manage/users/_id.vue index 331065ba637a..1ddf88ec57c9 100644 --- a/frontend/pages/admin/manage/users/_id.vue +++ b/frontend/pages/admin/manage/users/_id.vue @@ -27,6 +27,13 @@ label="User Group" :rules="[validators.required]" > +
+ + {{ $t("user.generate-password-reset-link") }} + + +
+ @@ -67,6 +74,9 @@ export default defineComponent({ const userError = ref(false); + const resetUrl = ref(null); + const generatingToken = ref(false); + onMounted(async () => { const { data, error } = await adminApi.users.getOne(userId); @@ -90,6 +100,20 @@ export default defineComponent({ } } + async function handlePasswordReset() { + if (user.value === null) return; + generatingToken.value = true; + + const { response, data } = await adminApi.users.generatePasswordResetToken({ email: user.value.email }); + + if (response?.status === 201 && data) { + const token: string = data.token; + resetUrl.value = `${window.location.origin}/reset-password?token=${token}`; + } + + generatingToken.value = false; + } + return { user, userError, @@ -98,6 +122,9 @@ export default defineComponent({ handleSubmit, groups, validators, + handlePasswordReset, + resetUrl, + generatingToken, }; }, }); diff --git a/mealie/routes/admin/admin_management_users.py b/mealie/routes/admin/admin_management_users.py index ff907c3b57a5..00bc84b59423 100644 --- a/mealie/routes/admin/admin_management_users.py +++ b/mealie/routes/admin/admin_management_users.py @@ -10,6 +10,8 @@ from mealie.schema.response.pagination import PaginationQuery from mealie.schema.response.responses import ErrorResponse from mealie.schema.user.auth import UnlockResults from mealie.schema.user.user import UserIn, UserOut, UserPagination +from mealie.schema.user.user_passwords import ForgotPassword, PasswordResetToken +from mealie.services.user_services.password_reset_service import PasswordResetService from mealie.services.user_services.user_service import UserService router = APIRouter(prefix="/users", tags=["Admin: Users"]) @@ -65,3 +67,12 @@ class AdminUserManagementRoutes(BaseAdminController): @router.delete("/{item_id}", response_model=UserOut) def delete_one(self, item_id: UUID4): return self.mixins.delete_one(item_id) + + @router.post("/password-reset-token", response_model=PasswordResetToken, status_code=201) + def generate_token(self, email: ForgotPassword): + """Generates a reset token and returns it. This is an authenticated endpoint""" + f_service = PasswordResetService(self.session) + token_entry = f_service.generate_reset_token(email.email) + if not token_entry: + raise HTTPException(status_code=500, detail=ErrorResponse.respond("error while generating reset token")) + return PasswordResetToken(token=token_entry.token) diff --git a/mealie/schema/user/user_passwords.py b/mealie/schema/user/user_passwords.py index f3f2de8ad782..4ae7a1436496 100644 --- a/mealie/schema/user/user_passwords.py +++ b/mealie/schema/user/user_passwords.py @@ -9,6 +9,10 @@ class ForgotPassword(MealieModel): email: str +class PasswordResetToken(MealieModel): + token: str + + class ValidateResetToken(MealieModel): token: str