feat(auth): change password and username (#1295)

* feat(backend): added change password api endpoint.

* feat(web): added profile UI to change password.

I think we can change the username too, but I don't know if we should for now disabled the username field.

* refactor: don't leak username or password.

* refactor: protect the route.

* generic

* feat: add ChangeUsername

* fix(tests): speculative fix for TestUserRepo_Update

* Revert "feat: add ChangeUsername"

This reverts commit d4c1645002883a278aa45dec3c8c19fa1cc75d9b.

* refactor into 1 endpoint that handles both

* feat: added option to change username as well. :pain:

* refactor: frontend

* refactor: function names in backend

I think this makes it more clear what their function is

* fix: change to 2 cols with separator

* refactor: update user

* fix: test db create user

---------

Co-authored-by: Kyle Sanderson <kyle.leet@gmail.com>
Co-authored-by: soup <soup@r4tio.dev>
Co-authored-by: martylukyy <35452459+martylukyy@users.noreply.github.com>
Co-authored-by: ze0s <ze0s@riseup.net>
This commit is contained in:
KaiserBh 2023-12-27 01:50:57 +11:00 committed by GitHub
parent d898b3cd8d
commit df2612602b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 390 additions and 57 deletions

View file

@ -0,0 +1,150 @@
/*
* Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import { useMutation } from "@tanstack/react-query";
import { APIClient } from "@api/APIClient";
import Toast from "@components/notifications/Toast";
import { Section } from "./_components";
import { Form, Formik } from "formik";
import { PasswordField, TextField } from "@components/inputs";
import { AuthContext } from "@utils/Context";
import toast from "react-hot-toast";
import { UserIcon } from "@heroicons/react/24/solid";
const AccountSettings = () => (
<Section
title="Account"
description="Manage account settings."
>
<div className="py-0.5">
<Credentials />
</div>
</Section>
);
interface InputValues {
username: string;
newUsername: string;
oldPassword: string;
newPassword: string;
confirmPassword: string;
}
function Credentials() {
const [ getAuthContext ] = AuthContext.use();
const validate = (values: InputValues) => {
const errors: Record<string, string> = {};
if (!values.username)
errors.username = "Required";
if (values.newPassword !== values.confirmPassword)
errors.confirmPassword = "Passwords don't match!";
return errors;
};
const logoutMutation = useMutation({
mutationFn: APIClient.auth.logout,
onSuccess: () => {
AuthContext.reset();
toast.custom((t) => (
<Toast type="success" body="User updated successfully. Please sign in again!" t={t} />
));
}
});
const updateUserMutation = useMutation({
mutationFn: (data: UserUpdate) => APIClient.auth.updateUser(data),
onSuccess: () => {
logoutMutation.mutate();
}
});
const separatorClass = "mb-6";
return (
<Section
title="Change credentials"
description="The username and password can be changed either separately or simultaneously. Note that you will be logged out after changing credentials."
noLeftPadding
>
<div className="px-2 pb-6 bg-white dark:bg-gray-800">
<Formik
initialValues={{
username: getAuthContext.username,
newUsername: "",
oldPassword: "",
newPassword: "",
confirmPassword: ""
}}
onSubmit={(data) => {
updateUserMutation.mutate({
username_current: data.username,
username_new: data.newUsername,
password_current: data.oldPassword,
password_new: data.newPassword,
});
}}
validate={validate}
>
{({ values }) => (
<Form>
<div className="grid grid-cols-2 gap-x-10">
<div className={separatorClass}>
<TextField name="username" label="Current Username" autoComplete="username" disabled />
</div>
<div className={separatorClass}>
<TextField name="newUsername" label="New Username" tooltip={
<div>
<p>Optional</p>
</div>
} />
</div>
<hr className="col-span-2 mb-6 border-t border-gray-300 dark:border-gray-750" />
<div className={separatorClass}>
<PasswordField name="oldPassword" placeholder="Required" label="Current Password" autoComplete="current-password" required tooltip={
<div>
<p>Required if updating credentials</p>
</div>
} />
</div>
<div>
<div className={separatorClass}>
<PasswordField name="newPassword" label="New Password" autoComplete="new-password" tooltip={
<div>
<p>Optional</p>
</div>
} />
</div>
{values.newPassword && (
<div className={separatorClass}>
<PasswordField name="confirmPassword" label="Confirm New Password" autoComplete="new-password" />
</div>
)}
</div>
</div>
<div className="flex justify-end">
<button
type="submit"
className="mt-4 w-auto flex items-center py-2 px-4 transition rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500"
>
<UserIcon className="w-4 h-4 mr-1" />
Save
</button>
</div>
</Form>
)}
</Formik>
</div>
</Section>
);
}
export default AccountSettings;

View file

@ -11,15 +11,22 @@ type SectionProps = {
description: string | React.ReactNode;
rightSide?: React.ReactNode;
children?: React.ReactNode;
noLeftPadding?: boolean;
};
export const Section = ({
title,
description,
rightSide,
children
children,
noLeftPadding = false,
}: SectionProps) => (
<div className="pb-6 px-4 lg:col-span-9">
<div
className={classNames(
"pb-6 px-4 lg:col-span-9",
noLeftPadding ? 'pl-0' : '',
)}
>
<div
className={classNames(
"mt-6 mb-4",

View file

@ -13,3 +13,4 @@ export { default as Logs } from "./Logs";
export { default as Notification } from "./Notifications";
export { default as Release } from "./Releases";
export { default as RegexPlayground } from "./RegexPlayground";
export { default as Account } from "./Account";