Password reset mostly working.

This commit is contained in:
Dessalines 2019-11-01 23:41:57 -07:00
parent 9f35b33dc7
commit 68e4b61808
15 changed files with 266 additions and 36 deletions

View file

@ -50,7 +50,7 @@ server {
client_max_body_size 50M;
location / {
rewrite (\/(user|u\/|inbox|post|community|c\/|create_post|create_community|login|search|setup|sponsors|communities|modlog|home)+) /static/index.html break;
rewrite (\/(user|u\/|inbox|post|community|c\/|create_post|create_community|login|search|setup|sponsors|communities|modlog|home|password_change)+) /static/index.html break;
proxy_pass http://0.0.0.0:8536;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;

7
docker/dev/.env vendored
View file

@ -2,9 +2,16 @@ DOMAIN=my_domain
DATABASE_PASSWORD=password
DATABASE_URL=postgres://lemmy:password@lemmy_db:5432/lemmy
JWT_SECRET=changeme
RATE_LIMIT_MESSAGE=30
RATE_LIMIT_MESSAGE_PER_SECOND=60
RATE_LIMIT_POST=3
RATE_LIMIT_POST_PER_SECOND=600
RATE_LIMIT_REGISTER=1
RATE_LIMIT_REGISTER_PER_SECOND=3600
# Optional email fields
SMTP_SERVER=
SMTP_LOGIN=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=Domain.com Lemmy Admin <notifications@domain.com>

17
docker/dev/Dockerfile vendored
View file

@ -10,27 +10,24 @@ RUN yarn install --pure-lockfile
COPY ui /app/ui
RUN yarn build
FROM rust:1.38 as rust
# Install musl
RUN apt-get update
RUN apt-get install musl-tools -y
RUN rustup target add x86_64-unknown-linux-musl
FROM ekidd/rust-musl-builder:1.38.0-openssl11 as rust
# Cache deps
WORKDIR /app
RUN sudo chown -R rust:rust .
RUN USER=root cargo new server
WORKDIR /app/server
COPY server/Cargo.toml server/Cargo.lock ./
RUN mkdir -p ./src/bin \
RUN sudo chown -R rust:rust .
RUN mkdir -p ./src/bin \
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-musl
RUN cargo build --release
RUN rm -f ./target/x86_64-unknown-linux-musl/release/deps/lemmy_server*
COPY server/src ./src/
COPY server/migrations ./migrations/
# build for release
RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --frozen --release --target=x86_64-unknown-linux-musl
# Build for release
RUN cargo build --frozen --release
# Get diesel-cli on there just in case
# RUN cargo install diesel_cli --no-default-features --features postgres

View file

@ -26,6 +26,10 @@ services:
- RATE_LIMIT_POST_PER_SECOND=${RATE_LIMIT_POST_PER_SECOND}
- RATE_LIMIT_REGISTER=${RATE_LIMIT_REGISTER}
- RATE_LIMIT_REGISTER_PER_SECOND=${RATE_LIMIT_REGISTER_PER_SECOND}
- SMTP_SERVER=${SMTP_SERVER}
- SMTP_LOGIN=${SMTP_LOGIN}
- SMTP_PASSWORD=${SMTP_PASSWORD}
- SMTP_FROM_ADDRESS=${SMTP_FROM_ADDRESS}
restart: always
depends_on:
- lemmy_db

37
server/Cargo.lock generated vendored
View file

@ -839,6 +839,11 @@ name = "futures"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "gcc"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "generic-array"
version = "0.12.3"
@ -1019,9 +1024,9 @@ dependencies = [
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lettre 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lettre_email 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)",
"strum 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1496,6 +1501,15 @@ dependencies = [
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand"
version = "0.4.6"
@ -1705,11 +1719,28 @@ dependencies = [
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rust-crypto"
version = "0.2.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rustc-demangle"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rustc-serialize"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rustc_version"
version = "0.2.3"
@ -2441,6 +2472,7 @@ dependencies = [
"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
"checksum futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "45dc39533a6cae6da2b56da48edae506bb767ec07370f86f70fc062e9d435869"
"checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
"checksum getrandom 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "e65cce4e5084b14874c4e7097f38cab54f47ee554f9194673456ea379dcc4c55"
"checksum h2 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)" = "a539b63339fbbb00e081e84b6e11bd1d9634a82d91da2984a18ac74a8823f392"
@ -2512,6 +2544,7 @@ dependencies = [
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
"checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8"
"checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
"checksum rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
"checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
"checksum rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d47eab0e83d9693d40f825f86948aa16eff6750ead4bdffc4ab95b8b3a7f052c"
@ -2534,7 +2567,9 @@ dependencies = [
"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e"
"checksum resolv-conf 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b263b4aa1b5de9ffc0054a2386f96992058bb6870aab516f8cdeb8a667d56dcb"
"checksum ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)" = "426bc186e3e95cac1e4a4be125a4aca7e84c2d616ffc02244eef36e2a60a093c"
"checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a"
"checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af"
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
"checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997"
"checksum safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"

3
server/Cargo.toml vendored
View file

@ -25,3 +25,6 @@ strum_macros = "0.15.0"
jsonwebtoken = "6.0.1"
regex = "1.1.9"
lazy_static = "1.3.0"
lettre = "0.9.2"
lettre_email = "0.9.2"
rust-crypto = "^0.2"

View file

@ -849,7 +849,7 @@ impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
let user_email = &user.email.expect("email");
let subject = &format!("Password reset for {}", user.name);
let hostname = Settings::get().hostname;
let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/{}>Click here to reset your password</a>", user.name, hostname, &token);
let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", user.name, hostname, &token);
match send_email(subject, user_email, &user.name, html) {
Ok(_o) => _o,
Err(_e) => {

View file

@ -1,8 +1,8 @@
use super::*;
use crate::schema::password_reset_request;
use crate::schema::password_reset_request::dsl::*;
use bcrypt::{hash, DEFAULT_COST};
use crypto::sha2::Sha256;
use crypto::digest::Digest;
#[derive(Queryable, Identifiable, PartialEq, Debug)]
#[table_name = "password_reset_request"]
@ -40,8 +40,9 @@ impl Crud<PasswordResetRequestForm> for PasswordResetRequest {
impl PasswordResetRequest {
pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> {
let token_hash =
hash(token, DEFAULT_COST).expect("Couldn't hash token");
let mut hasher = Sha256::new();
hasher.input_str(token);
let token_hash = hasher.result_str();
let form = PasswordResetRequestForm {
user_id: from_user_id,
@ -51,10 +52,13 @@ impl PasswordResetRequest {
Self::create(&conn, &form)
}
pub fn read_from_token(conn: &PgConnection, token: &str) -> Result<Self, Error> {
let token_hash =
hash(token, DEFAULT_COST).expect("Couldn't hash token");
password_reset_request.filter(token_encrypted.eq(token_hash)).first::<Self>(conn)
let mut hasher = Sha256::new();
hasher.input_str(token);
let token_hash = hasher.result_str();
password_reset_request
.filter(token_encrypted.eq(token_hash))
.filter(published.gt(now - 1.days()))
.first::<Self>(conn)
}
}

View file

@ -20,6 +20,7 @@ pub extern crate serde_json;
pub extern crate strum;
pub extern crate lettre;
pub extern crate lettre_email;
pub extern crate crypto;
pub mod api;
pub mod apub;
@ -63,7 +64,8 @@ impl Settings {
fn get() -> Self {
dotenv().ok();
let email_config = if env::var("SMTP_SERVER").is_ok() {
let email_config = if env::var("SMTP_SERVER").is_ok() &&
!env::var("SMTP_SERVER").unwrap().eq("") {
Some(EmailConfig {
smtp_server: env::var("SMTP_SERVER").expect("SMTP_SERVER must be set"),
smtp_login: env::var("SMTP_LOGIN").expect("SMTP_LOGIN must be set"),
@ -160,7 +162,6 @@ pub fn send_email(subject: &str, to_email: &str, to_username: &str, html: &str)
let email_config = Settings::get().email_config.ok_or("no_email_setup")?;
let email = Email::builder()
// .to((to_email, username))
.to((to_email, to_username))
.from((email_config.smtp_login.to_owned(), email_config.smtp_from_address))
.subject(subject)
@ -168,15 +169,15 @@ pub fn send_email(subject: &str, to_email: &str, to_username: &str, html: &str)
.build()
.unwrap();
let mut mailer = SmtpClient::new_simple(&email_config.smtp_server).unwrap()
.hello_name(ClientId::Domain("localhost".to_string()))
.credentials(Credentials::new(
email_config.smtp_login.to_owned(),
email_config.smtp_password.to_owned()))
.smtp_utf8(true)
.authentication_mechanism(Mechanism::Plain)
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.transport();
let mut mailer = SmtpClient::new_simple(&email_config.smtp_server).unwrap()
.hello_name(ClientId::Domain("localhost".to_string()))
.credentials(Credentials::new(
email_config.smtp_login.to_owned(),
email_config.smtp_password.to_owned()))
.smtp_utf8(true)
.authentication_mechanism(Mechanism::Plain)
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.transport();
let result = mailer.send(email.into());

View file

@ -9,7 +9,7 @@ import {
PasswordResetForm,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils';
import { msgOp, validEmail } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
@ -113,12 +113,13 @@ export class Login extends Component<any, State> {
class="form-control"
required
/>
<div
<button
disabled={!validEmail(this.state.loginForm.username_or_email)}
onClick={linkEvent(this, this.handlePasswordReset)}
class="pointer d-inline-block float-right text-muted small font-weight-bold"
className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
>
<T i18nKey="forgot_password">#</T>
</div>
</button>
</div>
</div>
<div class="form-group row">
@ -287,6 +288,7 @@ export class Login extends Component<any, State> {
}
handlePasswordReset(i: Login) {
event.preventDefault();
let resetForm: PasswordResetForm = {
email: i.state.loginForm.username_or_email,
};

160
ui/src/components/password_change.tsx vendored Normal file
View file

@ -0,0 +1,160 @@
import { Component, linkEvent } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import {
UserOperation,
LoginResponse,
PasswordChangeForm,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp, capitalizeFirstLetter } from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
interface State {
passwordChangeForm: PasswordChangeForm;
loading: boolean;
}
export class PasswordChange extends Component<any, State> {
private subscription: Subscription;
emptyState: State = {
passwordChangeForm: {
token: this.props.match.params.token,
password: undefined,
password_verify: undefined,
},
loading: false,
};
constructor(props: any, context: any) {
super(props, context);
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject
.pipe(
retryWhen(errors =>
errors.pipe(
delay(3000),
take(10)
)
)
)
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
componentDidMount() {
document.title = `${i18n.t('password_change')} - ${
WebSocketService.Instance.site.name
}`;
}
render() {
return (
<div class="container">
<div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>
<T i18nKey="password_change">#</T>
</h5>
{this.passwordChangeForm()}
</div>
</div>
</div>
);
}
passwordChangeForm() {
return (
<form onSubmit={linkEvent(this, this.handlePasswordChangeSubmit)}>
<div class="form-group row">
<label class="col-sm-2 col-form-label">
<T i18nKey="new_password">#</T>
</label>
<div class="col-sm-10">
<input
type="password"
value={this.state.passwordChangeForm.password}
onInput={linkEvent(this, this.handlePasswordChange)}
class="form-control"
required
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">
<T i18nKey="verify_password">#</T>
</label>
<div class="col-sm-10">
<input
type="password"
value={this.state.passwordChangeForm.password_verify}
onInput={linkEvent(this, this.handleVerifyPasswordChange)}
class="form-control"
required
/>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary">
{this.state.loading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
</svg>
) : (
capitalizeFirstLetter(i18n.t('save'))
)}
</button>
</div>
</div>
</form>
);
}
handlePasswordChange(i: PasswordChange, event: any) {
i.state.passwordChangeForm.password = event.target.value;
i.setState(i.state);
}
handleVerifyPasswordChange(i: PasswordChange, event: any) {
i.state.passwordChangeForm.password_verify = event.target.value;
i.setState(i.state);
}
handlePasswordChangeSubmit(i: PasswordChange, event: any) {
event.preventDefault();
i.state.loading = true;
i.setState(i.state);
WebSocketService.Instance.passwordChange(i.state.passwordChangeForm);
}
parseMessage(msg: any) {
let op: UserOperation = msgOp(msg);
if (msg.error) {
alert(i18n.t(msg.error));
this.state.loading = false;
this.setState(this.state);
return;
} else {
if (op == UserOperation.PasswordChange) {
this.state = this.emptyState;
this.setState(this.state);
let res: LoginResponse = msg;
UserService.Instance.login(res);
this.props.history.push('/');
}
}
}
}

5
ui/src/index.tsx vendored
View file

@ -7,6 +7,7 @@ import { Footer } from './components/footer';
import { Login } from './components/login';
import { CreatePost } from './components/create-post';
import { CreateCommunity } from './components/create-community';
import { PasswordChange } from './components/password_change';
import { Post } from './components/post';
import { Community } from './components/community';
import { Communities } from './components/communities';
@ -74,6 +75,10 @@ class Index extends Component<any, any> {
/>
<Route path={`/search`} component={Search} />
<Route path={`/sponsors`} component={Sponsors} />
<Route
path={`/password_change/:token`}
component={PasswordChange}
/>
</Switch>
<Symbols />
</div>

View file

@ -31,6 +31,7 @@ import {
UserSettingsForm,
DeleteAccountForm,
PasswordResetForm,
PasswordChangeForm,
} from '../interfaces';
import { webSocket } from 'rxjs/webSocket';
import { Subject } from 'rxjs';
@ -279,6 +280,10 @@ export class WebSocketService {
this.subject.next(this.wsSendWrapper(UserOperation.PasswordReset, form));
}
public passwordChange(form: PasswordChangeForm) {
this.subject.next(this.wsSendWrapper(UserOperation.PasswordChange, form));
}
private wsSendWrapper(op: UserOperation, data: any) {
let send = { op: UserOperation[op], data: data };
console.log(send);

View file

@ -118,6 +118,8 @@ export const en = {
verify_password: 'Verify Password',
forgot_password: 'forgot password',
reset_password_mail_sent: 'Sent an Email to reset your password.',
password_change: 'Password Change',
new_password: 'New Password',
no_email_setup: "This server hasn't correctly set up email.",
email: 'Email',
optional: 'Optional',

5
ui/src/utils.ts vendored
View file

@ -152,6 +152,11 @@ export function validURL(str: string) {
}
}
export function validEmail(email: string) {
let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}