Adding change password and email address from user settings.

- Fixes #384
- Fixes #385
pull/722/head
Dessalines 4 years ago
parent 4faa46ac29
commit f5a13717ea

19
README.md vendored

@ -257,16 +257,15 @@ If you'd like to add translations, take a look a look at the [English translatio
lang | done | missing
--- | --- | ---
de | 97% | avatar,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
eo | 84% | number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,are_you_sure,yes,no
es | 92% | avatar,archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
fr | 92% | avatar,archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
it | 93% | avatar,archive_link,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
nl | 86% | preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme
ru | 80% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
sv | 92% | avatar,archive_link,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
zh | 78% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
de | 97% | avatar,old_password,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
eo | 83% | number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,are_you_sure,yes,no
es | 92% | avatar,archive_link,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
fr | 92% | avatar,archive_link,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
it | 93% | avatar,archive_link,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
nl | 85% | preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme
ru | 79% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
sv | 92% | avatar,archive_link,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw
zh | 77% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no
If you'd like to update this report, run:

@ -0,0 +1,15 @@
-- user
drop view user_view;
create view user_view as
select id,
name,
avatar,
fedi_name,
admin,
banned,
published,
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
from user_ u;

@ -0,0 +1,16 @@
-- user
drop view user_view;
create view user_view as
select id,
name,
avatar,
email,
fedi_name,
admin,
banned,
published,
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
from user_ u;

@ -28,6 +28,10 @@ pub struct SaveUserSettings {
default_listing_type: i16,
lang: String,
avatar: Option<String>,
email: Option<String>,
new_password: Option<String>,
new_password_verify: Option<String>,
old_password: Option<String>,
auth: String,
}
@ -312,12 +316,45 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
let read_user = User_::read(&conn, user_id)?;
let email = match &data.email {
Some(email) => Some(email.to_owned()),
None => read_user.email,
};
let password_encrypted = match &data.new_password {
Some(new_password) => {
match &data.new_password_verify {
Some(new_password_verify) => {
// Make sure passwords match
if new_password != new_password_verify {
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
}
// Check the old password
match &data.old_password {
Some(old_password) => {
let valid: bool =
verify(old_password, &read_user.password_encrypted).unwrap_or(false);
if !valid {
return Err(APIError::err(&self.op, "password_incorrect"))?;
}
User_::update_password(&conn, user_id, &new_password)?.password_encrypted
}
None => return Err(APIError::err(&self.op, "password_incorrect"))?,
}
}
None => return Err(APIError::err(&self.op, "passwords_dont_match"))?,
}
}
None => read_user.password_encrypted,
};
let user_form = UserForm {
name: read_user.name,
fedi_name: read_user.fedi_name,
email: read_user.email,
email,
avatar: data.avatar.to_owned(),
password_encrypted: read_user.password_encrypted,
password_encrypted,
preferred_username: read_user.preferred_username,
updated: Some(naive_now()),
admin: read_user.admin,
@ -850,28 +887,8 @@ impl Perform<LoginResponse> for Oper<PasswordChange> {
return Err(APIError::err(&self.op, "passwords_dont_match"))?;
}
// Fetch the user
let read_user = User_::read(&conn, user_id)?;
// Update the user with the new password
let user_form = UserForm {
name: read_user.name,
fedi_name: read_user.fedi_name,
email: read_user.email,
avatar: read_user.avatar,
password_encrypted: data.password.to_owned(),
preferred_username: read_user.preferred_username,
updated: Some(naive_now()),
admin: read_user.admin,
banned: read_user.banned,
show_nsfw: read_user.show_nsfw,
theme: read_user.theme,
default_sort_type: read_user.default_sort_type,
default_listing_type: read_user.default_listing_type,
lang: read_user.lang,
};
let updated_user = match User_::update_password(&conn, user_id, &user_form) {
let updated_user = match User_::update_password(&conn, user_id, &data.password) {
Ok(user) => user,
Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user"))?,
};

@ -75,14 +75,13 @@ impl User_ {
pub fn update_password(
conn: &PgConnection,
user_id: i32,
form: &UserForm,
new_password: &str,
) -> Result<Self, Error> {
let mut edited_user = form.clone();
let password_hash =
hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
edited_user.password_encrypted = password_hash;
let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
Self::update(&conn, user_id, &edited_user)
diesel::update(user_.find(user_id))
.set(password_encrypted.eq(password_hash))
.get_result::<Self>(conn)
}
pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result<Self, Error> {

@ -7,6 +7,7 @@ table! {
id -> Int4,
name -> Varchar,
avatar -> Nullable<Text>,
email -> Nullable<Text>,
fedi_name -> Varchar,
admin -> Bool,
banned -> Bool,
@ -26,6 +27,7 @@ pub struct UserView {
pub id: i32,
pub name: String,
pub avatar: Option<String>,
pub email: Option<String>,
pub fedi_name: String,
pub admin: bool,
pub banned: bool,

@ -99,7 +99,6 @@ export class User extends Component<any, UserState> {
default_sort_type: null,
default_listing_type: null,
lang: null,
avatar: null,
auth: null,
},
userSettingsLoading: null,
@ -437,199 +436,240 @@ export class User extends Component<any, UserState> {
</h5>
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
<div class="form-group">
<div class="col-12">
<label>
<T i18nKey="avatar">#</T>
</label>
<form class="d-inline">
<label
htmlFor="file-upload"
class="pointer ml-4 text-muted small font-weight-bold"
>
<img
height="80"
width="80"
src={
this.state.userSettingsForm.avatar
? this.state.userSettingsForm.avatar
: 'https://via.placeholder.com/300/000?text=Avatar'
}
class="rounded-circle"
/>
</label>
<input
id="file-upload"
type="file"
accept="image/*,video/*"
name="file"
class="d-none"
disabled={!UserService.Instance.user}
onChange={linkEvent(this, this.handleImageUpload)}
<label>
<T i18nKey="avatar">#</T>
</label>
<form class="d-inline">
<label
htmlFor="file-upload"
class="pointer ml-4 text-muted small font-weight-bold"
>
<img
height="80"
width="80"
src={
this.state.userSettingsForm.avatar
? this.state.userSettingsForm.avatar
: 'https://via.placeholder.com/300/000?text=Avatar'
}
class="rounded-circle"
/>
</form>
</div>
</label>
<input
id="file-upload"
type="file"
accept="image/*,video/*"
name="file"
class="d-none"
disabled={!UserService.Instance.user}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>
</div>
<div class="form-group">
<div class="col-12">
<label>
<label>
<T i18nKey="language">#</T>
</label>
<select
value={this.state.userSettingsForm.lang}
onChange={linkEvent(this, this.handleUserSettingsLangChange)}
class="ml-2 custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="language">#</T>
</label>
<select
value={this.state.userSettingsForm.lang}
onChange={linkEvent(
</option>
<option value="browser">
<T i18nKey="browser_default">#</T>
</option>
<option disabled></option>
{languages.map(lang => (
<option value={lang.code}>{lang.name}</option>
))}
</select>
</div>
<div class="form-group">
<label>
<T i18nKey="theme">#</T>
</label>
<select
value={this.state.userSettingsForm.theme}
onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
class="ml-2 custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="theme">#</T>
</option>
{themes.map(theme => (
<option value={theme}>{theme}</option>
))}
</select>
</div>
<form className="form-group">
<label>
<T i18nKey="sort_type" class="mr-2">
#
</T>
</label>
<ListingTypeSelect
type_={this.state.userSettingsForm.default_listing_type}
onChange={this.handleUserSettingsListingTypeChange}
/>
</form>
<form className="form-group">
<label>
<T i18nKey="type" class="mr-2">
#
</T>
</label>
<SortSelect
sort={this.state.userSettingsForm.default_sort_type}
onChange={this.handleUserSettingsSortTypeChange}
/>
</form>
<div class="form-group row">
<label class="col-lg-3 col-form-label">
<T i18nKey="email">#</T>
</label>
<div class="col-lg-9">
<input
type="email"
class="form-control"
placeholder={i18n.t('optional')}
value={this.state.userSettingsForm.email}
onInput={linkEvent(
this,
this.handleUserSettingsLangChange
this.handleUserSettingsEmailChange
)}
class="ml-2 custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="language">#</T>
</option>
<option value="browser">
<T i18nKey="browser_default">#</T>
</option>
<option disabled></option>
{languages.map(lang => (
<option value={lang.code}>{lang.name}</option>
))}
</select>
minLength={3}
/>
</div>
</div>
<div class="form-group">
<div class="col-12">
<label>
<T i18nKey="theme">#</T>
</label>
<select
value={this.state.userSettingsForm.theme}
onChange={linkEvent(
<div class="form-group row">
<label class="col-lg-5 col-form-label">
<T i18nKey="new_password">#</T>
</label>
<div class="col-lg-7">
<input
type="password"
class="form-control"
value={this.state.userSettingsForm.new_password}
onInput={linkEvent(
this,
this.handleUserSettingsThemeChange
this.handleUserSettingsNewPasswordChange
)}
class="ml-2 custom-select custom-select-sm w-auto"
>
<option disabled>
<T i18nKey="theme">#</T>
</option>
{themes.map(theme => (
<option value={theme}>{theme}</option>
))}
</select>
/>
</div>
</div>
<form className="form-group">
<div class="col-12">
<label>
<T i18nKey="sort_type" class="mr-2">
#
</T>
</label>
<ListingTypeSelect
type_={this.state.userSettingsForm.default_listing_type}
onChange={this.handleUserSettingsListingTypeChange}
<div class="form-group row">
<label class="col-lg-5 col-form-label">
<T i18nKey="verify_password">#</T>
</label>
<div class="col-lg-7">
<input
type="password"
class="form-control"
value={this.state.userSettingsForm.new_password_verify}
onInput={linkEvent(
this,
this.handleUserSettingsNewPasswordVerifyChange
)}
/>
</div>
</form>
<form className="form-group">
<div class="col-12">
<label>
<T i18nKey="type" class="mr-2">
#
</T>
</label>
<SortSelect
sort={this.state.userSettingsForm.default_sort_type}
onChange={this.handleUserSettingsSortTypeChange}
</div>
<div class="form-group row">
<label class="col-lg-5 col-form-label">
<T i18nKey="old_password">#</T>
</label>
<div class="col-lg-7">
<input
type="password"
class="form-control"
value={this.state.userSettingsForm.old_password}
onInput={linkEvent(
this,
this.handleUserSettingsOldPasswordChange
)}
/>
</div>
</form>
</div>
{WebSocketService.Instance.site.enable_nsfw && (
<div class="form-group">
<div class="col-12">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.userSettingsForm.show_nsfw}
onChange={linkEvent(
this,
this.handleUserSettingsShowNsfwChange
)}
/>
<label class="form-check-label">
<T i18nKey="show_nsfw">#</T>
</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.userSettingsForm.show_nsfw}
onChange={linkEvent(
this,
this.handleUserSettingsShowNsfwChange
)}
/>
<label class="form-check-label">
<T i18nKey="show_nsfw">#</T>
</label>
</div>
</div>
)}
<div class="form-group">
<div class="col-12">
<button
type="submit"
class="btn btn-block btn-secondary mr-4"
>
{this.state.userSettingsLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
capitalizeFirstLetter(i18n.t('save'))
)}
</button>
</div>
<button type="submit" class="btn btn-block btn-secondary mr-4">
{this.state.userSettingsLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
capitalizeFirstLetter(i18n.t('save'))
)}
</button>
</div>
<hr />
<div class="form-group mb-0">
<div class="col-12">
<button
class="btn btn-block btn-danger"
onClick={linkEvent(
this,
this.handleDeleteAccountShowConfirmToggle
)}
>
<T i18nKey="delete_account">#</T>
</button>
{this.state.deleteAccountShowConfirm && (
<>
<div class="my-2 alert alert-danger" role="alert">
<T i18nKey="delete_account_confirm">#</T>
</div>
<input
type="password"
value={this.state.deleteAccountForm.password}
onInput={linkEvent(
this,
this.handleDeleteAccountPasswordChange
)}
class="form-control my-2"
/>
<button
class="btn btn-danger mr-4"
disabled={!this.state.deleteAccountForm.password}
onClick={linkEvent(this, this.handleDeleteAccount)}
>
{this.state.deleteAccountLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
capitalizeFirstLetter(i18n.t('delete'))
)}
</button>
<button
class="btn btn-secondary"
onClick={linkEvent(
this,
this.handleDeleteAccountShowConfirmToggle
)}
>
<T i18nKey="cancel">#</T>
</button>
</>
<button
class="btn btn-block btn-danger"
onClick={linkEvent(
this,
this.handleDeleteAccountShowConfirmToggle
)}
</div>
>
<T i18nKey="delete_account">#</T>
</button>
{this.state.deleteAccountShowConfirm && (
<>
<div class="my-2 alert alert-danger" role="alert">
<T i18nKey="delete_account_confirm">#</T>
</div>
<input
type="password"
value={this.state.deleteAccountForm.password}
onInput={linkEvent(
this,
this.handleDeleteAccountPasswordChange
)}
class="form-control my-2"
/>
<button
class="btn btn-danger mr-4"
disabled={!this.state.deleteAccountForm.password}
onClick={linkEvent(this, this.handleDeleteAccount)}
>
{this.state.deleteAccountLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
capitalizeFirstLetter(i18n.t('delete'))
)}
</button>
<button
class="btn btn-secondary"
onClick={linkEvent(
this,
this.handleDeleteAccountShowConfirmToggle
)}
>
<T i18nKey="cancel">#</T>
</button>
</>
)}
</div>
</form>
</div>
@ -786,6 +826,38 @@ export class User extends Component<any, UserState> {
this.setState(this.state);
}
handleUserSettingsEmailChange(i: User, event: any) {
i.state.userSettingsForm.email = event.target.value;
if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
i.state.userSettingsForm.email = undefined;
}
i.setState(i.state);
}
handleUserSettingsNewPasswordChange(i: User, event: any) {
i.state.userSettingsForm.new_password = event.target.value;
if (i.state.userSettingsForm.new_password == '') {
i.state.userSettingsForm.new_password = undefined;
}
i.setState(i.state);
}
handleUserSettingsNewPasswordVerifyChange(i: User, event: any) {
i.state.userSettingsForm.new_password_verify = event.target.value;
if (i.state.userSettingsForm.new_password_verify == '') {
i.state.userSettingsForm.new_password_verify = undefined;
}
i.setState(i.state);
}
handleUserSettingsOldPasswordChange(i: User, event: any) {
i.state.userSettingsForm.old_password = event.target.value;
if (i.state.userSettingsForm.old_password == '') {
i.state.userSettingsForm.old_password = undefined;
}
i.setState(i.state);
}
handleImageUpload(i: User, event: any) {
event.preventDefault();
let file = event.target.files[0];
@ -856,6 +928,8 @@ export class User extends Component<any, UserState> {
if (msg.error) {
alert(i18n.t(msg.error));
this.state.deleteAccountLoading = false;
this.state.avatarLoading = false;
this.state.userSettingsLoading = false;
if (msg.error == 'couldnt_find_that_username_or_email') {
this.context.router.history.push('/');
}
@ -882,6 +956,7 @@ export class User extends Component<any, UserState> {
UserService.Instance.user.default_listing_type;
this.state.userSettingsForm.lang = UserService.Instance.user.lang;
this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
this.state.userSettingsForm.email = this.state.user.email;
}
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
window.scrollTo(0, 0);

@ -87,6 +87,7 @@ export interface UserView {
id: number;
name: string;
avatar?: string;
email?: string;
fedi_name: string;
published: string;
number_of_posts: number;
@ -481,6 +482,10 @@ export interface UserSettingsForm {
default_listing_type: ListingType;
lang: string;
avatar?: string;
email?: string;
new_password?: string;
new_password_verify?: string;
old_password?: string;
auth: string;
}

@ -118,6 +118,7 @@ export const en = {
unread_messages: 'Unread Messages',
password: 'Password',
verify_password: 'Verify Password',
old_password: 'Old Password',
forgot_password: 'forgot password',
reset_password_mail_sent: 'Sent an Email to reset your password.',
password_change: 'Password Change',

Loading…
Cancel
Save