]> Untitled Git - lemmy-ui.git/blob - src/shared/components/login.tsx
Change from using Link to NavLink. resolve #269
[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                 autoComplete="email"
121                 required
122                 minLength={3}
123               />
124             </div>
125           </div>
126           <div class="form-group row">
127             <label class="col-sm-2 col-form-label" htmlFor="login-password">
128               {i18n.t("password")}
129             </label>
130             <div class="col-sm-10">
131               <input
132                 type="password"
133                 id="login-password"
134                 value={this.state.loginForm.password}
135                 onInput={linkEvent(this, this.handleLoginPasswordChange)}
136                 class="form-control"
137                 autoComplete="current-password"
138                 required
139                 maxLength={60}
140               />
141               <button
142                 type="button"
143                 onClick={linkEvent(this, this.handlePasswordReset)}
144                 className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold pointer-events not-allowed"
145                 disabled={!validEmail(this.state.loginForm.username_or_email)}
146                 title={i18n.t("no_password_reset")}
147               >
148                 {i18n.t("forgot_password")}
149               </button>
150             </div>
151           </div>
152           <div class="form-group row">
153             <div class="col-sm-10">
154               <button type="submit" class="btn btn-secondary">
155                 {this.state.loginLoading ? <Spinner /> : i18n.t("login")}
156               </button>
157             </div>
158           </div>
159         </form>
160       </div>
161     );
162   }
163
164   registerForm() {
165     return (
166       <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
167         <h5>{i18n.t("sign_up")}</h5>
168
169         <div class="form-group row">
170           <label class="col-sm-2 col-form-label" htmlFor="register-username">
171             {i18n.t("username")}
172           </label>
173
174           <div class="col-sm-10">
175             <input
176               type="text"
177               id="register-username"
178               class="form-control"
179               value={this.state.registerForm.username}
180               onInput={linkEvent(this, this.handleRegisterUsernameChange)}
181               required
182               minLength={3}
183               maxLength={20}
184               pattern="[a-zA-Z0-9_]+"
185             />
186           </div>
187         </div>
188
189         <div class="form-group row">
190           <label class="col-sm-2 col-form-label" htmlFor="register-email">
191             {i18n.t("email")}
192           </label>
193           <div class="col-sm-10">
194             <input
195               type="email"
196               id="register-email"
197               class="form-control"
198               placeholder={i18n.t("optional")}
199               value={this.state.registerForm.email}
200               autoComplete="email"
201               onInput={linkEvent(this, this.handleRegisterEmailChange)}
202               minLength={3}
203             />
204             {!validEmail(this.state.registerForm.email) && (
205               <div class="mt-2 mb-0 alert alert-light" role="alert">
206                 <Icon icon="alert-triangle" classes="icon-inline mr-2" />
207                 {i18n.t("no_password_reset")}
208               </div>
209             )}
210           </div>
211         </div>
212
213         <div class="form-group row">
214           <label class="col-sm-2 col-form-label" htmlFor="register-password">
215             {i18n.t("password")}
216           </label>
217           <div class="col-sm-10">
218             <input
219               type="password"
220               id="register-password"
221               value={this.state.registerForm.password}
222               autoComplete="new-password"
223               onInput={linkEvent(this, this.handleRegisterPasswordChange)}
224               maxLength={60}
225               class="form-control"
226               required
227             />
228           </div>
229         </div>
230
231         <div class="form-group row">
232           <label
233             class="col-sm-2 col-form-label"
234             htmlFor="register-verify-password"
235           >
236             {i18n.t("verify_password")}
237           </label>
238           <div class="col-sm-10">
239             <input
240               type="password"
241               id="register-verify-password"
242               value={this.state.registerForm.password_verify}
243               autoComplete="new-password"
244               onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
245               maxLength={60}
246               class="form-control"
247               required
248             />
249           </div>
250         </div>
251
252         {this.state.captcha && (
253           <div class="form-group row">
254             <label class="col-sm-2" htmlFor="register-captcha">
255               <span class="mr-2">{i18n.t("enter_code")}</span>
256               <button
257                 type="button"
258                 class="btn btn-secondary"
259                 onClick={linkEvent(this, this.handleRegenCaptcha)}
260                 aria-label={i18n.t("captcha")}
261               >
262                 <Icon icon="refresh-cw" classes="icon-refresh-cw" />
263               </button>
264             </label>
265             {this.showCaptcha()}
266             <div class="col-sm-6">
267               <input
268                 type="text"
269                 class="form-control"
270                 id="register-captcha"
271                 value={this.state.registerForm.captcha_answer}
272                 onInput={linkEvent(
273                   this,
274                   this.handleRegisterCaptchaAnswerChange
275                 )}
276                 required
277               />
278             </div>
279           </div>
280         )}
281         {this.state.site_view.site.enable_nsfw && (
282           <div class="form-group row">
283             <div class="col-sm-10">
284               <div class="form-check">
285                 <input
286                   class="form-check-input"
287                   id="register-show-nsfw"
288                   type="checkbox"
289                   checked={this.state.registerForm.show_nsfw}
290                   onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
291                 />
292                 <label class="form-check-label" htmlFor="register-show-nsfw">
293                   {i18n.t("show_nsfw")}
294                 </label>
295               </div>
296             </div>
297           </div>
298         )}
299         <div class="form-group row">
300           <div class="col-sm-10">
301             <button type="submit" class="btn btn-secondary">
302               {this.state.registerLoading ? <Spinner /> : i18n.t("sign_up")}
303             </button>
304           </div>
305         </div>
306       </form>
307     );
308   }
309
310   showCaptcha() {
311     return (
312       <div class="col-sm-4">
313         {this.state.captcha.ok && (
314           <>
315             <img
316               class="rounded-top img-fluid"
317               src={this.captchaPngSrc()}
318               style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
319               alt={i18n.t("captcha")}
320             />
321             {this.state.captcha.ok.wav && (
322               <button
323                 class="rounded-bottom btn btn-sm btn-secondary btn-block"
324                 style="border-top-right-radius: 0; border-top-left-radius: 0;"
325                 title={i18n.t("play_captcha_audio")}
326                 onClick={linkEvent(this, this.handleCaptchaPlay)}
327                 type="button"
328                 disabled={this.state.captchaPlaying}
329               >
330                 <Icon icon="play" classes="icon-play" />
331               </button>
332             )}
333           </>
334         )}
335       </div>
336     );
337   }
338
339   handleLoginSubmit(i: Login, event: any) {
340     event.preventDefault();
341     i.state.loginLoading = true;
342     i.setState(i.state);
343     WebSocketService.Instance.send(wsClient.login(i.state.loginForm));
344   }
345
346   handleLoginUsernameChange(i: Login, event: any) {
347     i.state.loginForm.username_or_email = event.target.value;
348     i.setState(i.state);
349   }
350
351   handleLoginPasswordChange(i: Login, event: any) {
352     i.state.loginForm.password = event.target.value;
353     i.setState(i.state);
354   }
355
356   handleRegisterSubmit(i: Login, event: any) {
357     event.preventDefault();
358     i.state.registerLoading = true;
359     i.setState(i.state);
360     WebSocketService.Instance.send(wsClient.register(i.state.registerForm));
361   }
362
363   handleRegisterUsernameChange(i: Login, event: any) {
364     i.state.registerForm.username = event.target.value;
365     i.setState(i.state);
366   }
367
368   handleRegisterEmailChange(i: Login, event: any) {
369     i.state.registerForm.email = event.target.value;
370     if (i.state.registerForm.email == "") {
371       i.state.registerForm.email = undefined;
372     }
373     i.setState(i.state);
374   }
375
376   handleRegisterPasswordChange(i: Login, event: any) {
377     i.state.registerForm.password = event.target.value;
378     i.setState(i.state);
379   }
380
381   handleRegisterPasswordVerifyChange(i: Login, event: any) {
382     i.state.registerForm.password_verify = event.target.value;
383     i.setState(i.state);
384   }
385
386   handleRegisterShowNsfwChange(i: Login, event: any) {
387     i.state.registerForm.show_nsfw = event.target.checked;
388     i.setState(i.state);
389   }
390
391   handleRegisterCaptchaAnswerChange(i: Login, event: any) {
392     i.state.registerForm.captcha_answer = event.target.value;
393     i.setState(i.state);
394   }
395
396   handleRegenCaptcha(_i: Login, event: any) {
397     event.preventDefault();
398     WebSocketService.Instance.send(wsClient.getCaptcha());
399   }
400
401   handlePasswordReset(i: Login, event: any) {
402     event.preventDefault();
403     let resetForm: PasswordReset = {
404       email: i.state.loginForm.username_or_email,
405     };
406     WebSocketService.Instance.send(wsClient.passwordReset(resetForm));
407   }
408
409   handleCaptchaPlay(i: Login, event: any) {
410     event.preventDefault();
411     let snd = new Audio("data:audio/wav;base64," + i.state.captcha.ok.wav);
412     snd.play();
413     i.state.captchaPlaying = true;
414     i.setState(i.state);
415     snd.addEventListener("ended", () => {
416       snd.currentTime = 0;
417       i.state.captchaPlaying = false;
418       i.setState(this.state);
419     });
420   }
421
422   captchaPngSrc() {
423     return `data:image/png;base64,${this.state.captcha.ok.png}`;
424   }
425
426   parseMessage(msg: any) {
427     let op = wsUserOp(msg);
428     console.log(msg);
429     if (msg.error) {
430       toast(i18n.t(msg.error), "danger");
431       this.state = this.emptyState;
432       this.state.registerForm.captcha_answer = undefined;
433       // Refetch another captcha
434       WebSocketService.Instance.send(wsClient.getCaptcha());
435       this.setState(this.state);
436       return;
437     } else {
438       if (op == UserOperation.Login) {
439         let data = wsJsonToRes<LoginResponse>(msg).data;
440         this.state = this.emptyState;
441         this.setState(this.state);
442         UserService.Instance.login(data);
443         WebSocketService.Instance.send(
444           wsClient.userJoin({
445             auth: authField(),
446           })
447         );
448         toast(i18n.t("logged_in"));
449         this.props.history.push("/");
450       } else if (op == UserOperation.Register) {
451         let data = wsJsonToRes<LoginResponse>(msg).data;
452         this.state = this.emptyState;
453         this.setState(this.state);
454         UserService.Instance.login(data);
455         WebSocketService.Instance.send(
456           wsClient.userJoin({
457             auth: authField(),
458           })
459         );
460         this.props.history.push("/communities");
461       } else if (op == UserOperation.GetCaptcha) {
462         let data = wsJsonToRes<GetCaptchaResponse>(msg).data;
463         if (data.ok) {
464           this.state.captcha = data;
465           this.state.registerForm.captcha_uuid = data.ok.uuid;
466           this.setState(this.state);
467         }
468       } else if (op == UserOperation.PasswordReset) {
469         toast(i18n.t("reset_password_mail_sent"));
470       } else if (op == UserOperation.GetSite) {
471         let data = wsJsonToRes<GetSiteResponse>(msg).data;
472         this.state.site_view = data.site_view;
473         this.setState(this.state);
474       }
475     }
476   }
477 }