]> Untitled Git - lemmy-ui.git/blob - src/shared/components/home/login.tsx
Navigate away from login page if already logged in. (#429)
[lemmy-ui.git] / src / shared / components / home / login.tsx
1 import { Options, passwordStrength } from "check-password-strength";
2 import { I18nKeys } from "i18next";
3 import { Component, linkEvent } from "inferno";
4 import { T } from "inferno-i18next-dess";
5 import {
6   GetCaptchaResponse,
7   GetSiteResponse,
8   Login as LoginForm,
9   LoginResponse,
10   PasswordReset,
11   Register,
12   SiteView,
13   UserOperation,
14 } from "lemmy-js-client";
15 import { Subscription } from "rxjs";
16 import { i18n } from "../../i18next";
17 import { UserService, WebSocketService } from "../../services";
18 import {
19   authField,
20   isBrowser,
21   joinLemmyUrl,
22   setIsoData,
23   toast,
24   validEmail,
25   wsClient,
26   wsJsonToRes,
27   wsSubscribe,
28   wsUserOp,
29 } from "../../utils";
30 import { HtmlTags } from "../common/html-tags";
31 import { Icon, Spinner } from "../common/icon";
32
33 const passwordStrengthOptions: Options<string> = [
34   {
35     id: 0,
36     value: "too_weak",
37     minDiversity: 0,
38     minLength: 0,
39   },
40   {
41     id: 1,
42     value: "weak",
43     minDiversity: 2,
44     minLength: 10,
45   },
46   {
47     id: 2,
48     value: "medium",
49     minDiversity: 3,
50     minLength: 12,
51   },
52   {
53     id: 3,
54     value: "strong",
55     minDiversity: 4,
56     minLength: 14,
57   },
58 ];
59
60 interface State {
61   loginForm: LoginForm;
62   registerForm: Register;
63   loginLoading: boolean;
64   registerLoading: boolean;
65   captcha: GetCaptchaResponse;
66   captchaPlaying: boolean;
67   site_view: SiteView;
68 }
69
70 export class Login extends Component<any, State> {
71   private isoData = setIsoData(this.context);
72   private subscription: Subscription;
73   private audio: HTMLAudioElement;
74
75   emptyState: State = {
76     loginForm: {
77       username_or_email: undefined,
78       password: undefined,
79     },
80     registerForm: {
81       username: undefined,
82       password: undefined,
83       password_verify: undefined,
84       show_nsfw: false,
85       captcha_uuid: undefined,
86       captcha_answer: undefined,
87     },
88     loginLoading: false,
89     registerLoading: false,
90     captcha: undefined,
91     captchaPlaying: false,
92     site_view: this.isoData.site_res.site_view,
93   };
94
95   constructor(props: any, context: any) {
96     super(props, context);
97
98     this.state = this.emptyState;
99
100     this.parseMessage = this.parseMessage.bind(this);
101     this.subscription = wsSubscribe(this.parseMessage);
102
103     if (isBrowser()) {
104       WebSocketService.Instance.send(wsClient.getCaptcha());
105     }
106   }
107
108   componentDidMount() {
109     // Navigate to home if already logged in
110     if (UserService.Instance.myUserInfo) {
111       this.context.router.history.push("/");
112     }
113   }
114
115   componentWillUnmount() {
116     if (isBrowser()) {
117       this.subscription.unsubscribe();
118     }
119   }
120
121   get documentTitle(): string {
122     return `${i18n.t("login")} - ${this.state.site_view.site.name}`;
123   }
124
125   get isLemmyMl(): boolean {
126     return isBrowser() && window.location.hostname == "lemmy.ml";
127   }
128
129   render() {
130     return (
131       <div class="container">
132         <HtmlTags
133           title={this.documentTitle}
134           path={this.context.router.route.match.url}
135         />
136         <div class="row">
137           <div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
138           <div class="col-12 col-lg-6">{this.registerForm()}</div>
139         </div>
140       </div>
141     );
142   }
143
144   loginForm() {
145     return (
146       <div>
147         <form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
148           <h5>{i18n.t("login")}</h5>
149           <div class="form-group row">
150             <label
151               class="col-sm-2 col-form-label"
152               htmlFor="login-email-or-username"
153             >
154               {i18n.t("email_or_username")}
155             </label>
156             <div class="col-sm-10">
157               <input
158                 type="text"
159                 class="form-control"
160                 id="login-email-or-username"
161                 value={this.state.loginForm.username_or_email}
162                 onInput={linkEvent(this, this.handleLoginUsernameChange)}
163                 autoComplete="email"
164                 required
165                 minLength={3}
166               />
167             </div>
168           </div>
169           <div class="form-group row">
170             <label class="col-sm-2 col-form-label" htmlFor="login-password">
171               {i18n.t("password")}
172             </label>
173             <div class="col-sm-10">
174               <input
175                 type="password"
176                 id="login-password"
177                 value={this.state.loginForm.password}
178                 onInput={linkEvent(this, this.handleLoginPasswordChange)}
179                 class="form-control"
180                 autoComplete="current-password"
181                 required
182                 maxLength={60}
183               />
184               <button
185                 type="button"
186                 onClick={linkEvent(this, this.handlePasswordReset)}
187                 className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold pointer-events not-allowed"
188                 disabled={!validEmail(this.state.loginForm.username_or_email)}
189                 title={i18n.t("no_password_reset")}
190               >
191                 {i18n.t("forgot_password")}
192               </button>
193             </div>
194           </div>
195           <div class="form-group row">
196             <div class="col-sm-10">
197               <button type="submit" class="btn btn-secondary">
198                 {this.state.loginLoading ? <Spinner /> : i18n.t("login")}
199               </button>
200             </div>
201           </div>
202         </form>
203       </div>
204     );
205   }
206
207   registerForm() {
208     return (
209       <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
210         <h5>{i18n.t("sign_up")}</h5>
211
212         <div class="form-group row">
213           <label class="col-sm-2 col-form-label" htmlFor="register-username">
214             {i18n.t("username")}
215           </label>
216
217           <div class="col-sm-10">
218             <input
219               type="text"
220               id="register-username"
221               class="form-control"
222               value={this.state.registerForm.username}
223               onInput={linkEvent(this, this.handleRegisterUsernameChange)}
224               required
225               minLength={3}
226               pattern="[a-zA-Z0-9_]+"
227               title={i18n.t("community_reqs")}
228             />
229           </div>
230         </div>
231
232         <div class="form-group row">
233           <label class="col-sm-2 col-form-label" htmlFor="register-email">
234             {i18n.t("email")}
235           </label>
236           <div class="col-sm-10">
237             <input
238               type="email"
239               id="register-email"
240               class="form-control"
241               placeholder={i18n.t("optional")}
242               value={this.state.registerForm.email}
243               autoComplete="email"
244               onInput={linkEvent(this, this.handleRegisterEmailChange)}
245               minLength={3}
246             />
247             {!validEmail(this.state.registerForm.email) && (
248               <div class="mt-2 mb-0 alert alert-light" role="alert">
249                 <Icon icon="alert-triangle" classes="icon-inline mr-2" />
250                 {i18n.t("no_password_reset")}
251               </div>
252             )}
253           </div>
254         </div>
255
256         <div class="form-group row">
257           <label class="col-sm-2 col-form-label" htmlFor="register-password">
258             {i18n.t("password")}
259           </label>
260           <div class="col-sm-10">
261             <input
262               type="password"
263               id="register-password"
264               value={this.state.registerForm.password}
265               autoComplete="new-password"
266               onInput={linkEvent(this, this.handleRegisterPasswordChange)}
267               minLength={10}
268               maxLength={60}
269               class="form-control"
270               required
271             />
272             {this.state.registerForm.password && (
273               <div class={this.passwordColorClass}>
274                 {i18n.t(this.passwordStrength as I18nKeys)}
275               </div>
276             )}
277           </div>
278         </div>
279
280         <div class="form-group row">
281           <label
282             class="col-sm-2 col-form-label"
283             htmlFor="register-verify-password"
284           >
285             {i18n.t("verify_password")}
286           </label>
287           <div class="col-sm-10">
288             <input
289               type="password"
290               id="register-verify-password"
291               value={this.state.registerForm.password_verify}
292               autoComplete="new-password"
293               onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
294               maxLength={60}
295               class="form-control"
296               required
297             />
298           </div>
299         </div>
300
301         {this.state.captcha && (
302           <div class="form-group row">
303             <label class="col-sm-2" htmlFor="register-captcha">
304               <span class="mr-2">{i18n.t("enter_code")}</span>
305               <button
306                 type="button"
307                 class="btn btn-secondary"
308                 onClick={linkEvent(this, this.handleRegenCaptcha)}
309                 aria-label={i18n.t("captcha")}
310               >
311                 <Icon icon="refresh-cw" classes="icon-refresh-cw" />
312               </button>
313             </label>
314             {this.showCaptcha()}
315             <div class="col-sm-6">
316               <input
317                 type="text"
318                 class="form-control"
319                 id="register-captcha"
320                 value={this.state.registerForm.captcha_answer}
321                 onInput={linkEvent(
322                   this,
323                   this.handleRegisterCaptchaAnswerChange
324                 )}
325                 required
326               />
327             </div>
328           </div>
329         )}
330         {this.state.site_view.site.enable_nsfw && (
331           <div class="form-group row">
332             <div class="col-sm-10">
333               <div class="form-check">
334                 <input
335                   class="form-check-input"
336                   id="register-show-nsfw"
337                   type="checkbox"
338                   checked={this.state.registerForm.show_nsfw}
339                   onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
340                 />
341                 <label class="form-check-label" htmlFor="register-show-nsfw">
342                   {i18n.t("show_nsfw")}
343                 </label>
344               </div>
345             </div>
346           </div>
347         )}
348         {this.isLemmyMl && (
349           <div class="mt-2 mb-0 alert alert-light" role="alert">
350             <T i18nKey="lemmy_ml_registration_message">
351               #<a href={joinLemmyUrl}>#</a>
352             </T>
353           </div>
354         )}
355         <div class="form-group row">
356           <div class="col-sm-10">
357             <button type="submit" class="btn btn-secondary">
358               {this.state.registerLoading ? <Spinner /> : i18n.t("sign_up")}
359             </button>
360           </div>
361         </div>
362       </form>
363     );
364   }
365
366   showCaptcha() {
367     return (
368       <div class="col-sm-4">
369         {this.state.captcha.ok && (
370           <>
371             <img
372               class="rounded-top img-fluid"
373               src={this.captchaPngSrc()}
374               style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
375               alt={i18n.t("captcha")}
376             />
377             {this.state.captcha.ok.wav && (
378               <button
379                 class="rounded-bottom btn btn-sm btn-secondary btn-block"
380                 style="border-top-right-radius: 0; border-top-left-radius: 0;"
381                 title={i18n.t("play_captcha_audio")}
382                 onClick={linkEvent(this, this.handleCaptchaPlay)}
383                 type="button"
384                 disabled={this.state.captchaPlaying}
385               >
386                 <Icon icon="play" classes="icon-play" />
387               </button>
388             )}
389           </>
390         )}
391       </div>
392     );
393   }
394
395   get passwordStrength() {
396     return passwordStrength(
397       this.state.registerForm.password,
398       passwordStrengthOptions
399     ).value;
400   }
401
402   get passwordColorClass(): string {
403     let strength = this.passwordStrength;
404
405     if (["weak", "medium"].includes(strength)) {
406       return "text-warning";
407     } else if (strength == "strong") {
408       return "text-success";
409     } else {
410       return "text-danger";
411     }
412   }
413
414   handleLoginSubmit(i: Login, event: any) {
415     event.preventDefault();
416     i.state.loginLoading = true;
417     i.setState(i.state);
418     WebSocketService.Instance.send(wsClient.login(i.state.loginForm));
419   }
420
421   handleLoginUsernameChange(i: Login, event: any) {
422     i.state.loginForm.username_or_email = event.target.value;
423     i.setState(i.state);
424   }
425
426   handleLoginPasswordChange(i: Login, event: any) {
427     i.state.loginForm.password = event.target.value;
428     i.setState(i.state);
429   }
430
431   handleRegisterSubmit(i: Login, event: any) {
432     event.preventDefault();
433     i.state.registerLoading = true;
434     i.setState(i.state);
435     WebSocketService.Instance.send(wsClient.register(i.state.registerForm));
436   }
437
438   handleRegisterUsernameChange(i: Login, event: any) {
439     i.state.registerForm.username = event.target.value;
440     i.setState(i.state);
441   }
442
443   handleRegisterEmailChange(i: Login, event: any) {
444     i.state.registerForm.email = event.target.value;
445     if (i.state.registerForm.email == "") {
446       i.state.registerForm.email = undefined;
447     }
448     i.setState(i.state);
449   }
450
451   handleRegisterPasswordChange(i: Login, event: any) {
452     i.state.registerForm.password = event.target.value;
453     i.setState(i.state);
454   }
455
456   handleRegisterPasswordVerifyChange(i: Login, event: any) {
457     i.state.registerForm.password_verify = event.target.value;
458     i.setState(i.state);
459   }
460
461   handleRegisterShowNsfwChange(i: Login, event: any) {
462     i.state.registerForm.show_nsfw = event.target.checked;
463     i.setState(i.state);
464   }
465
466   handleRegisterCaptchaAnswerChange(i: Login, event: any) {
467     i.state.registerForm.captcha_answer = event.target.value;
468     i.setState(i.state);
469   }
470
471   handleRegenCaptcha(i: Login) {
472     i.audio = null;
473     i.state.captchaPlaying = false;
474     i.setState(i.state);
475     WebSocketService.Instance.send(wsClient.getCaptcha());
476   }
477
478   handlePasswordReset(i: Login, event: any) {
479     event.preventDefault();
480     let resetForm: PasswordReset = {
481       email: i.state.loginForm.username_or_email,
482     };
483     WebSocketService.Instance.send(wsClient.passwordReset(resetForm));
484   }
485
486   handleCaptchaPlay(i: Login) {
487     // This was a bad bug, it should only build the new audio on a new file.
488     // Replays would stop prematurely if this was rebuilt every time.
489     if (i.audio == null) {
490       let base64 = `data:audio/wav;base64,${i.state.captcha.ok.wav}`;
491       i.audio = new Audio(base64);
492     }
493
494     i.audio.play();
495
496     i.state.captchaPlaying = true;
497     i.setState(i.state);
498
499     i.audio.addEventListener("ended", () => {
500       i.audio.currentTime = 0;
501       i.state.captchaPlaying = false;
502       i.setState(i.state);
503     });
504   }
505
506   captchaPngSrc() {
507     return `data:image/png;base64,${this.state.captcha.ok.png}`;
508   }
509
510   parseMessage(msg: any) {
511     let op = wsUserOp(msg);
512     console.log(msg);
513     if (msg.error) {
514       toast(i18n.t(msg.error), "danger");
515       this.state = this.emptyState;
516       this.state.registerForm.captcha_answer = undefined;
517       // Refetch another captcha
518       WebSocketService.Instance.send(wsClient.getCaptcha());
519       this.setState(this.state);
520       return;
521     } else {
522       if (op == UserOperation.Login) {
523         let data = wsJsonToRes<LoginResponse>(msg).data;
524         this.state = this.emptyState;
525         this.setState(this.state);
526         UserService.Instance.login(data);
527         WebSocketService.Instance.send(
528           wsClient.userJoin({
529             auth: authField(),
530           })
531         );
532         toast(i18n.t("logged_in"));
533         this.props.history.push("/");
534       } else if (op == UserOperation.Register) {
535         let data = wsJsonToRes<LoginResponse>(msg).data;
536         this.state = this.emptyState;
537         this.setState(this.state);
538         UserService.Instance.login(data);
539         WebSocketService.Instance.send(
540           wsClient.userJoin({
541             auth: authField(),
542           })
543         );
544         this.props.history.push("/communities");
545       } else if (op == UserOperation.GetCaptcha) {
546         let data = wsJsonToRes<GetCaptchaResponse>(msg).data;
547         if (data.ok) {
548           this.state.captcha = data;
549           this.state.registerForm.captcha_uuid = data.ok.uuid;
550           this.setState(this.state);
551         }
552       } else if (op == UserOperation.PasswordReset) {
553         toast(i18n.t("reset_password_mail_sent"));
554       } else if (op == UserOperation.GetSite) {
555         let data = wsJsonToRes<GetSiteResponse>(msg).data;
556         this.state.site_view = data.site_view;
557         this.setState(this.state);
558       }
559     }
560   }
561 }