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