1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-23 07:09:41 +02:00

feature: add password reset token endpoint to the admin panel (#2171)

* add password reset token endpoint to the admin panel

* add None check on token

* add localization message for passowrd reset link button
This commit is contained in:
Carter 2023-03-12 15:33:36 -05:00 committed by GitHub
parent 1b26ca0cb3
commit 93eb2af087
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 52 additions and 1 deletions

View file

@ -689,6 +689,7 @@
"error-cannot-delete-super-user": "Error! Cannot Delete Super User", "error-cannot-delete-super-user": "Error! Cannot Delete Super User",
"existing-password-does-not-match": "Existing password does not match", "existing-password-does-not-match": "Existing password does not match",
"full-name": "Full Name", "full-name": "Full Name",
"generate-password-reset-link": "Generate Password Reset Link",
"invite-only": "Invite Only", "invite-only": "Invite Only",
"link-id": "Link ID", "link-id": "Link ID",
"link-name": "Link Name", "link-name": "Link Name",

View file

@ -1,5 +1,5 @@
import { BaseCRUDAPI } from "../base/base-clients"; 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"; const prefix = "/api";
@ -7,6 +7,7 @@ const routes = {
adminUsers: `${prefix}/admin/users`, adminUsers: `${prefix}/admin/users`,
adminUsersId: (tag: string) => `${prefix}/admin/users/${tag}`, adminUsersId: (tag: string) => `${prefix}/admin/users/${tag}`,
adminResetLockedUsers: (force: boolean) => `${prefix}/admin/users/unlock?force=${force ? "true" : "false"}`, adminResetLockedUsers: (force: boolean) => `${prefix}/admin/users/unlock?force=${force ? "true" : "false"}`,
adminPasswordResetToken: `${prefix}/admin/users/password-reset-token`,
}; };
export class AdminUsersApi extends BaseCRUDAPI<UserIn, UserOut, UserOut> { export class AdminUsersApi extends BaseCRUDAPI<UserIn, UserOut, UserOut> {
@ -16,4 +17,8 @@ export class AdminUsersApi extends BaseCRUDAPI<UserIn, UserOut, UserOut> {
async unlockAllUsers(force = false) { async unlockAllUsers(force = false) {
return await this.requests.post<UnlockResults>(routes.adminResetLockedUsers(force), {}); return await this.requests.post<UnlockResults>(routes.adminResetLockedUsers(force), {});
} }
async generatePasswordResetToken(payload: ForgotPassword) {
return await this.requests.post<PasswordResetToken>(routes.adminPasswordResetToken, payload);
}
} }

View file

@ -233,3 +233,6 @@ export interface UserIn {
export interface ValidateResetToken { export interface ValidateResetToken {
token: string; token: string;
} }
export interface PasswordResetToken {
token: string;
}

View file

@ -27,6 +27,13 @@
label="User Group" label="User Group"
:rules="[validators.required]" :rules="[validators.required]"
></v-select> ></v-select>
<div class="d-flex py-2 pr-2">
<BaseButton type="button" :loading="generatingToken" create @click.prevent="handlePasswordReset">
{{ $t("user.generate-password-reset-link") }}
</BaseButton>
<AppButtonCopy v-if="resetUrl" :copy-text="resetUrl"></AppButtonCopy>
</div>
<AutoForm v-model="user" :items="userForm" update-mode /> <AutoForm v-model="user" :items="userForm" update-mode />
</v-card-text> </v-card-text>
</v-card> </v-card>
@ -67,6 +74,9 @@ export default defineComponent({
const userError = ref(false); const userError = ref(false);
const resetUrl = ref<string | null>(null);
const generatingToken = ref(false);
onMounted(async () => { onMounted(async () => {
const { data, error } = await adminApi.users.getOne(userId); 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 { return {
user, user,
userError, userError,
@ -98,6 +122,9 @@ export default defineComponent({
handleSubmit, handleSubmit,
groups, groups,
validators, validators,
handlePasswordReset,
resetUrl,
generatingToken,
}; };
}, },
}); });

View file

@ -10,6 +10,8 @@ from mealie.schema.response.pagination import PaginationQuery
from mealie.schema.response.responses import ErrorResponse from mealie.schema.response.responses import ErrorResponse
from mealie.schema.user.auth import UnlockResults from mealie.schema.user.auth import UnlockResults
from mealie.schema.user.user import UserIn, UserOut, UserPagination 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 from mealie.services.user_services.user_service import UserService
router = APIRouter(prefix="/users", tags=["Admin: Users"]) router = APIRouter(prefix="/users", tags=["Admin: Users"])
@ -65,3 +67,12 @@ class AdminUserManagementRoutes(BaseAdminController):
@router.delete("/{item_id}", response_model=UserOut) @router.delete("/{item_id}", response_model=UserOut)
def delete_one(self, item_id: UUID4): def delete_one(self, item_id: UUID4):
return self.mixins.delete_one(item_id) 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)

View file

@ -9,6 +9,10 @@ class ForgotPassword(MealieModel):
email: str email: str
class PasswordResetToken(MealieModel):
token: str
class ValidateResetToken(MealieModel): class ValidateResetToken(MealieModel):
token: str token: str