]> Untitled Git - lemmy.git/commitdiff
Password reset mostly working.
authorDessalines <tyhou13@gmx.com>
Sat, 2 Nov 2019 06:41:57 +0000 (23:41 -0700)
committerDessalines <tyhou13@gmx.com>
Sat, 2 Nov 2019 06:41:57 +0000 (23:41 -0700)
15 files changed:
ansible/templates/nginx.conf
docker/dev/.env
docker/dev/Dockerfile
docker/dev/docker-compose.yml
server/Cargo.lock
server/Cargo.toml
server/src/api/user.rs
server/src/db/password_reset_request.rs
server/src/lib.rs
ui/src/components/login.tsx
ui/src/components/password_change.tsx [new file with mode: 0644]
ui/src/index.tsx
ui/src/services/WebSocketService.ts
ui/src/translations/en.ts
ui/src/utils.ts

index ec25439a528995cfdc04d015294f9cd83a776417..e39f1e1c1a44a546f52af5df7d9051c32cd1fabe 100644 (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;
index cca4deae7f7be5469305406d2968ba0138034b95..cf809d894366512ab4fdf0b7a38f8237a892c921 100644 (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>
index 5d25e48bd36147f6fb61cd54e9bc81aedf6c65a1..203643e1d8143bfc6d1aa3948ca79bfcb3362315 100644 (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
index 2a7a88ecd7295bc22ed6ac3f7d3490e374f9b825..c38515aa38f50340e26c8535d00b800ebe088018 100644 (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
index e28b0f92a2c3a11f027c7a1c2d7d3a4b75effc6b..5f9d783846a29d7b15eed510e8ae1da114c393d0 100644 (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"
index a8964e1729d59b9e1c957ec615b82be198633928..3f555829b871c2f3e107c1b310c044e24abe6718 100644 (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"
index 469c38a7235aea9bc6369195ce0943a86baf7f87..ee4070c5a8e97ff3e5e1a4d40ac7366c1e9c011a 100644 (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) => {
index e9968aa8ac4a435c3b1863790efc44686f1e9a08..6720bd7d340b946573918409f0ce50edef3a3d0f 100644 (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)
   }
 }
 
index b06f29be310936c09392a86f2a8ea1b5b1a16cc6..653a6fefc01bf572de5fd2db602b92df72406f41 100644 (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());
 
index c2db7ee60d37d3c840bb19481d53686fcd0f74fb..8d0df3e33b0c1cce2615c122a50e86edc31b0ffe 100644 (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,
     };
diff --git a/ui/src/components/password_change.tsx b/ui/src/components/password_change.tsx
new file mode 100644 (file)
index 0000000..3e542f7
--- /dev/null
@@ -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('/');
+      }
+    }
+  }
+}
index f3c7ff38ab64cf88b4a3b6d723421a7443df311b..2e50db882660cf7318f0645e67ab9400bf72b941 100644 (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>
index c77816df2fe57c5311e1300be683414de2b3c97b..34da585083211da549c272bf95b593829c7d9f1d 100644 (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);
index 4e0d81dbbdb72d1e32026c9abda897d14c00b3d0..f73e0d0989fb978eded7edd4064bb38b3ff2f99a 100644 (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',
index b9220a2d7a99c3e44c26ffa00c8498edaa72179b..9d2e720eb6ddf829315f2bf3ff894c352a24e693 100644 (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);
 }