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