diff --git a/ansible/templates/nginx.conf b/ansible/templates/nginx.conf index ec25439a5..e39f1e1c1 100644 --- a/ansible/templates/nginx.conf +++ b/ansible/templates/nginx.conf @@ -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; diff --git a/docker/dev/.env b/docker/dev/.env index cca4deae7..cf809d894 100644 --- a/docker/dev/.env +++ b/docker/dev/.env @@ -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 diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index 5d25e48bd..203643e1d 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -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 diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index 2a7a88ecd..c38515aa3 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -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 diff --git a/server/Cargo.lock b/server/Cargo.lock index e28b0f92a..5f9d78384 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -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" diff --git a/server/Cargo.toml b/server/Cargo.toml index a8964e172..3f555829b 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -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" diff --git a/server/src/api/user.rs b/server/src/api/user.rs index 469c38a72..ee4070c5a 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -849,7 +849,7 @@ impl Perform for Oper { let user_email = &user.email.expect("email"); let subject = &format!("Password reset for {}", user.name); let hostname = Settings::get().hostname; - let html = &format!("

Password Reset Request for {}


Click here to reset your password", user.name, hostname, &token); + let html = &format!("

Password Reset Request for {}


Click here to reset your password", user.name, hostname, &token); match send_email(subject, user_email, &user.name, html) { Ok(_o) => _o, Err(_e) => { diff --git a/server/src/db/password_reset_request.rs b/server/src/db/password_reset_request.rs index e9968aa8a..6720bd7d3 100644 --- a/server/src/db/password_reset_request.rs +++ b/server/src/db/password_reset_request.rs @@ -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 for PasswordResetRequest { impl PasswordResetRequest { pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result { - 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 { - let token_hash = - hash(token, DEFAULT_COST).expect("Couldn't hash token"); - - password_reset_request.filter(token_encrypted.eq(token_hash)).first::(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::(conn) } } diff --git a/server/src/lib.rs b/server/src/lib.rs index b06f29be3..653a6fefc 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -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()); diff --git a/ui/src/components/login.tsx b/ui/src/components/login.tsx index c2db7ee60..8d0df3e33 100644 --- a/ui/src/components/login.tsx +++ b/ui/src/components/login.tsx @@ -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 { class="form-control" required /> -
# -
+
@@ -287,6 +288,7 @@ export class Login extends Component { } handlePasswordReset(i: Login) { + event.preventDefault(); let resetForm: PasswordResetForm = { email: i.state.loginForm.username_or_email, }; diff --git a/ui/src/components/password_change.tsx b/ui/src/components/password_change.tsx new file mode 100644 index 000000000..3e542f7b5 --- /dev/null +++ b/ui/src/components/password_change.tsx @@ -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 { + 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 ( +
+
+
+
+ # +
+ {this.passwordChangeForm()} +
+
+
+ ); + } + + passwordChangeForm() { + return ( +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+ ); + } + + 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('/'); + } + } + } +} diff --git a/ui/src/index.tsx b/ui/src/index.tsx index f3c7ff38a..2e50db882 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -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 { /> +
diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index c77816df2..34da58508 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -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); diff --git a/ui/src/translations/en.ts b/ui/src/translations/en.ts index 4e0d81dbb..f73e0d098 100644 --- a/ui/src/translations/en.ts +++ b/ui/src/translations/en.ts @@ -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', diff --git a/ui/src/utils.ts b/ui/src/utils.ts index b9220a2d7..9d2e720eb 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -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); }