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