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