mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04: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:
parent
1b26ca0cb3
commit
93eb2af087
@ -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",
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,3 +233,6 @@ export interface UserIn {
|
|||||||
export interface ValidateResetToken {
|
export interface ValidateResetToken {
|
||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
export interface PasswordResetToken {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user