]> Untitled Git - lemmy.git/blobdiff - ui/src/components/login.tsx
routes.api: fix get_captcha endpoint (#1135)
[lemmy.git] / ui / src / components / login.tsx
index 8d0df3e33b0c1cce2615c122a50e86edc31b0ffe..caf8c9cfb48c9463f04d149b7f0700c07231a94e 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
@@ -7,17 +8,23 @@ import {
   LoginResponse,
   UserOperation,
   PasswordResetForm,
-} from '../interfaces';
+  GetSiteResponse,
+  GetCaptchaResponse,
+  WebSocketJsonResponse,
+  Site,
+} from 'lemmy-js-client';
 import { WebSocketService, UserService } from '../services';
-import { msgOp, validEmail } from '../utils';
+import { wsJsonToRes, validEmail, toast } from '../utils';
 import { i18n } from '../i18next';
-import { T } from 'inferno-i18next';
 
 interface State {
   loginForm: LoginForm;
   registerForm: RegisterForm;
   loginLoading: boolean;
   registerLoading: boolean;
+  captcha: GetCaptchaResponse;
+  captchaPlaying: boolean;
+  site: Site;
 }
 
 export class Login extends Component<any, State> {
@@ -34,9 +41,27 @@ export class Login extends Component<any, State> {
       password_verify: undefined,
       admin: false,
       show_nsfw: false,
+      captcha_uuid: undefined,
+      captcha_answer: undefined,
     },
     loginLoading: false,
     registerLoading: false,
+    captcha: undefined,
+    captchaPlaying: false,
+    site: {
+      id: undefined,
+      name: undefined,
+      creator_id: undefined,
+      published: undefined,
+      creator_name: undefined,
+      number_of_users: undefined,
+      number_of_posts: undefined,
+      number_of_comments: undefined,
+      number_of_communities: undefined,
+      enable_downvotes: undefined,
+      open_registration: undefined,
+      enable_nsfw: undefined,
+    },
   };
 
   constructor(props: any, context: any) {
@@ -45,34 +70,33 @@ export class Login extends Component<any, State> {
     this.state = this.emptyState;
 
     this.subscription = WebSocketService.Instance.subject
-      .pipe(
-        retryWhen(errors =>
-          errors.pipe(
-            delay(3000),
-            take(10)
-          )
-        )
-      )
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
         msg => this.parseMessage(msg),
         err => console.error(err),
         () => console.log('complete')
       );
+
+    WebSocketService.Instance.getSite();
+    WebSocketService.Instance.getCaptcha();
   }
 
   componentWillUnmount() {
     this.subscription.unsubscribe();
   }
 
-  componentDidMount() {
-    document.title = `${i18n.t('login')} - ${
-      WebSocketService.Instance.site.name
-    }`;
+  get documentTitle(): string {
+    if (this.state.site.name) {
+      return `${i18n.t('login')} - ${this.state.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
   }
 
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle} />
         <div class="row">
           <div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
           <div class="col-12 col-lg-6">{this.registerForm()}</div>
@@ -85,15 +109,19 @@ export class Login extends Component<any, State> {
     return (
       <div>
         <form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
-          <h5>Login</h5>
+          <h5>{i18n.t('login')}</h5>
           <div class="form-group row">
-            <label class="col-sm-2 col-form-label">
-              <T i18nKey="email_or_username">#</T>
+            <label
+              class="col-sm-2 col-form-label"
+              htmlFor="login-email-or-username"
+            >
+              {i18n.t('email_or_username')}
             </label>
             <div class="col-sm-10">
               <input
                 type="text"
                 class="form-control"
+                id="login-email-or-username"
                 value={this.state.loginForm.username_or_email}
                 onInput={linkEvent(this, this.handleLoginUsernameChange)}
                 required
@@ -102,24 +130,27 @@ export class Login extends Component<any, State> {
             </div>
           </div>
           <div class="form-group row">
-            <label class="col-sm-2 col-form-label">
-              <T i18nKey="password">#</T>
+            <label class="col-sm-2 col-form-label" htmlFor="login-password">
+              {i18n.t('password')}
             </label>
             <div class="col-sm-10">
               <input
                 type="password"
+                id="login-password"
                 value={this.state.loginForm.password}
                 onInput={linkEvent(this, this.handleLoginPasswordChange)}
                 class="form-control"
                 required
               />
-              <button
-                disabled={!validEmail(this.state.loginForm.username_or_email)}
-                onClick={linkEvent(this, this.handlePasswordReset)}
-                className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
-              >
-                <T i18nKey="forgot_password">#</T>
-              </button>
+              {validEmail(this.state.loginForm.username_or_email) && (
+                <button
+                  type="button"
+                  onClick={linkEvent(this, this.handlePasswordReset)}
+                  className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
+                >
+                  {i18n.t('forgot_password')}
+                </button>
+              )}
             </div>
           </div>
           <div class="form-group row">
@@ -139,19 +170,21 @@ export class Login extends Component<any, State> {
       </div>
     );
   }
+
   registerForm() {
     return (
       <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
-        <h5>
-          <T i18nKey="sign_up">#</T>
-        </h5>
+        <h5>{i18n.t('sign_up')}</h5>
+
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">
-            <T i18nKey="username">#</T>
+          <label class="col-sm-2 col-form-label" htmlFor="register-username">
+            {i18n.t('username')}
           </label>
+
           <div class="col-sm-10">
             <input
               type="text"
+              id="register-username"
               class="form-control"
               value={this.state.registerForm.username}
               onInput={linkEvent(this, this.handleRegisterUsernameChange)}
@@ -162,64 +195,117 @@ export class Login extends Component<any, State> {
             />
           </div>
         </div>
+
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">
-            <T i18nKey="email">#</T>
+          <label class="col-sm-2 col-form-label" htmlFor="register-email">
+            {i18n.t('email')}
           </label>
           <div class="col-sm-10">
             <input
               type="email"
+              id="register-email"
               class="form-control"
               placeholder={i18n.t('optional')}
               value={this.state.registerForm.email}
               onInput={linkEvent(this, this.handleRegisterEmailChange)}
               minLength={3}
             />
+            {!validEmail(this.state.registerForm.email) && (
+              <div class="mt-2 mb-0 alert alert-light" role="alert">
+                <svg class="icon icon-inline mr-2">
+                  <use xlinkHref="#icon-alert-triangle"></use>
+                </svg>
+                {i18n.t('no_password_reset')}
+              </div>
+            )}
           </div>
         </div>
+
         <div class="form-group row">
-          <label class="col-sm-2 col-form-label">
-            <T i18nKey="password">#</T>
+          <label class="col-sm-2 col-form-label" htmlFor="register-password">
+            {i18n.t('password')}
           </label>
           <div class="col-sm-10">
             <input
               type="password"
+              id="register-password"
               value={this.state.registerForm.password}
+              autoComplete="new-password"
               onInput={linkEvent(this, this.handleRegisterPasswordChange)}
               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
+            class="col-sm-2 col-form-label"
+            htmlFor="register-verify-password"
+          >
+            {i18n.t('verify_password')}
           </label>
           <div class="col-sm-10">
             <input
               type="password"
+              id="register-verify-password"
               value={this.state.registerForm.password_verify}
+              autoComplete="new-password"
               onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
               class="form-control"
               required
             />
           </div>
         </div>
-        <div class="form-group row">
-          <div class="col-sm-10">
-            <div class="form-check">
+
+        {this.state.captcha && (
+          <div class="form-group row">
+            <label class="col-sm-2" htmlFor="register-captcha">
+              <span class="mr-2">{i18n.t('enter_code')}</span>
+              <button
+                type="button"
+                class="btn btn-secondary"
+                onClick={linkEvent(this, this.handleRegenCaptcha)}
+              >
+                <svg class="icon icon-refresh-cw">
+                  <use xlinkHref="#icon-refresh-cw"></use>
+                </svg>
+              </button>
+            </label>
+            {this.showCaptcha()}
+            <div class="col-sm-6">
               <input
-                class="form-check-input"
-                type="checkbox"
-                checked={this.state.registerForm.show_nsfw}
-                onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
+                type="text"
+                class="form-control"
+                id="register-captcha"
+                value={this.state.registerForm.captcha_answer}
+                onInput={linkEvent(
+                  this,
+                  this.handleRegisterCaptchaAnswerChange
+                )}
+                required
               />
-              <label class="form-check-label">
-                <T i18nKey="show_nsfw">#</T>
-              </label>
             </div>
           </div>
-        </div>
+        )}
+        {this.state.site.enable_nsfw && (
+          <div class="form-group row">
+            <div class="col-sm-10">
+              <div class="form-check">
+                <input
+                  class="form-check-input"
+                  id="register-show-nsfw"
+                  type="checkbox"
+                  checked={this.state.registerForm.show_nsfw}
+                  onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
+                />
+                <label class="form-check-label" htmlFor="register-show-nsfw">
+                  {i18n.t('show_nsfw')}
+                </label>
+              </div>
+            </div>
+          </div>
+        )}
         <div class="form-group row">
           <div class="col-sm-10">
             <button type="submit" class="btn btn-secondary">
@@ -237,6 +323,36 @@ export class Login extends Component<any, State> {
     );
   }
 
+  showCaptcha() {
+    return (
+      <div class="col-sm-4">
+        {this.state.captcha.ok && (
+          <>
+            <img
+              class="rounded-top img-fluid"
+              src={this.captchaPngSrc()}
+              style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
+            />
+            {this.state.captcha.ok.wav && (
+              <button
+                class="rounded-bottom btn btn-sm btn-secondary btn-block"
+                style="border-top-right-radius: 0; border-top-left-radius: 0;"
+                title={i18n.t('play_captcha_audio')}
+                onClick={linkEvent(this, this.handleCaptchaPlay)}
+                type="button"
+                disabled={this.state.captchaPlaying}
+              >
+                <svg class="icon icon-play">
+                  <use xlinkHref="#icon-play"></use>
+                </svg>
+              </button>
+            )}
+          </>
+        )}
+      </div>
+    );
+  }
+
   handleLoginSubmit(i: Login, event: any) {
     event.preventDefault();
     i.state.loginLoading = true;
@@ -258,7 +374,6 @@ export class Login extends Component<any, State> {
     event.preventDefault();
     i.state.registerLoading = true;
     i.setState(i.state);
-
     WebSocketService.Instance.register(i.state.registerForm);
   }
 
@@ -269,6 +384,9 @@ export class Login extends Component<any, State> {
 
   handleRegisterEmailChange(i: Login, event: any) {
     i.state.registerForm.email = event.target.value;
+    if (i.state.registerForm.email == '') {
+      i.state.registerForm.email = undefined;
+    }
     i.setState(i.state);
   }
 
@@ -287,6 +405,16 @@ export class Login extends Component<any, State> {
     i.setState(i.state);
   }
 
+  handleRegisterCaptchaAnswerChange(i: Login, event: any) {
+    i.state.registerForm.captcha_answer = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleRegenCaptcha(_i: Login, _event: any) {
+    event.preventDefault();
+    WebSocketService.Instance.getCaptcha();
+  }
+
   handlePasswordReset(i: Login) {
     event.preventDefault();
     let resetForm: PasswordResetForm = {
@@ -295,28 +423,62 @@ export class Login extends Component<any, State> {
     WebSocketService.Instance.passwordReset(resetForm);
   }
 
-  parseMessage(msg: any) {
-    let op: UserOperation = msgOp(msg);
+  handleCaptchaPlay(i: Login) {
+    event.preventDefault();
+    let snd = new Audio('data:audio/wav;base64,' + i.state.captcha.ok.wav);
+    snd.play();
+    i.state.captchaPlaying = true;
+    i.setState(i.state);
+    snd.addEventListener('ended', () => {
+      snd.currentTime = 0;
+      i.state.captchaPlaying = false;
+      i.setState(this.state);
+    });
+  }
+
+  captchaPngSrc() {
+    return `data:image/png;base64,${this.state.captcha.ok.png}`;
+  }
+
+  parseMessage(msg: WebSocketJsonResponse) {
+    let res = wsJsonToRes(msg);
     if (msg.error) {
-      alert(i18n.t(msg.error));
+      toast(i18n.t(msg.error), 'danger');
       this.state = this.emptyState;
+      this.state.registerForm.captcha_answer = undefined;
+      // Refetch another captcha
+      WebSocketService.Instance.getCaptcha();
       this.setState(this.state);
       return;
     } else {
-      if (op == UserOperation.Login) {
+      if (res.op == UserOperation.Login) {
+        let data = res.data as LoginResponse;
         this.state = this.emptyState;
         this.setState(this.state);
-        let res: LoginResponse = msg;
-        UserService.Instance.login(res);
+        UserService.Instance.login(data);
+        WebSocketService.Instance.userJoin();
+        toast(i18n.t('logged_in'));
         this.props.history.push('/');
-      } else if (op == UserOperation.Register) {
+      } else if (res.op == UserOperation.Register) {
+        let data = res.data as LoginResponse;
         this.state = this.emptyState;
         this.setState(this.state);
-        let res: LoginResponse = msg;
-        UserService.Instance.login(res);
+        UserService.Instance.login(data);
+        WebSocketService.Instance.userJoin();
         this.props.history.push('/communities');
-      } else if (op == UserOperation.PasswordReset) {
-        alert(i18n.t('reset_password_mail_sent'));
+      } else if (res.op == UserOperation.GetCaptcha) {
+        let data = res.data as GetCaptchaResponse;
+        if (data.ok) {
+          this.state.captcha = data;
+          this.state.registerForm.captcha_uuid = data.ok.uuid;
+          this.setState(this.state);
+        }
+      } else if (res.op == UserOperation.PasswordReset) {
+        toast(i18n.t('reset_password_mail_sent'));
+      } else if (res.op == UserOperation.GetSite) {
+        let data = res.data as GetSiteResponse;
+        this.state.site = data.site;
+        this.setState(this.state);
       }
     }
   }