]> Untitled Git - lemmy.git/blob - ui/src/components/login.tsx
Some password reset UI fixes. Fixes #955
[lemmy.git] / ui / src / components / login.tsx
1 import { Component, linkEvent } from 'inferno';
2 import { Subscription } from 'rxjs';
3 import { retryWhen, delay, take } from 'rxjs/operators';
4 import {
5   LoginForm,
6   RegisterForm,
7   LoginResponse,
8   UserOperation,
9   PasswordResetForm,
10   GetSiteResponse,
11   WebSocketJsonResponse,
12 } from '../interfaces';
13 import { WebSocketService, UserService } from '../services';
14 import { wsJsonToRes, validEmail, toast } from '../utils';
15 import { i18n } from '../i18next';
16
17 interface State {
18   loginForm: LoginForm;
19   registerForm: RegisterForm;
20   loginLoading: boolean;
21   registerLoading: boolean;
22   enable_nsfw: boolean;
23   mathQuestion: {
24     a: number;
25     b: number;
26     answer: number;
27   };
28 }
29
30 export class Login extends Component<any, State> {
31   private subscription: Subscription;
32
33   emptyState: State = {
34     loginForm: {
35       username_or_email: undefined,
36       password: undefined,
37     },
38     registerForm: {
39       username: undefined,
40       password: undefined,
41       password_verify: undefined,
42       admin: false,
43       show_nsfw: false,
44     },
45     loginLoading: false,
46     registerLoading: false,
47     enable_nsfw: undefined,
48     mathQuestion: {
49       a: Math.floor(Math.random() * 10) + 1,
50       b: Math.floor(Math.random() * 10) + 1,
51       answer: undefined,
52     },
53   };
54
55   constructor(props: any, context: any) {
56     super(props, context);
57
58     this.state = this.emptyState;
59
60     this.subscription = WebSocketService.Instance.subject
61       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
62       .subscribe(
63         msg => this.parseMessage(msg),
64         err => console.error(err),
65         () => console.log('complete')
66       );
67
68     WebSocketService.Instance.getSite();
69   }
70
71   componentWillUnmount() {
72     this.subscription.unsubscribe();
73   }
74
75   render() {
76     return (
77       <div class="container">
78         <div class="row">
79           <div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
80           <div class="col-12 col-lg-6">{this.registerForm()}</div>
81         </div>
82       </div>
83     );
84   }
85
86   loginForm() {
87     return (
88       <div>
89         <form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
90           <h5>{i18n.t('login')}</h5>
91           <div class="form-group row">
92             <label
93               class="col-sm-2 col-form-label"
94               htmlFor="login-email-or-username"
95             >
96               {i18n.t('email_or_username')}
97             </label>
98             <div class="col-sm-10">
99               <input
100                 type="text"
101                 class="form-control"
102                 id="login-email-or-username"
103                 value={this.state.loginForm.username_or_email}
104                 onInput={linkEvent(this, this.handleLoginUsernameChange)}
105                 required
106                 minLength={3}
107               />
108             </div>
109           </div>
110           <div class="form-group row">
111             <label class="col-sm-2 col-form-label" htmlFor="login-password">
112               {i18n.t('password')}
113             </label>
114             <div class="col-sm-10">
115               <input
116                 type="password"
117                 id="login-password"
118                 value={this.state.loginForm.password}
119                 onInput={linkEvent(this, this.handleLoginPasswordChange)}
120                 class="form-control"
121                 required
122               />
123               {validEmail(this.state.loginForm.username_or_email) && (
124                 <button
125                   type="button"
126                   onClick={linkEvent(this, this.handlePasswordReset)}
127                   className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"
128                 >
129                   {i18n.t('forgot_password')}
130                 </button>
131               )}
132             </div>
133           </div>
134           <div class="form-group row">
135             <div class="col-sm-10">
136               <button type="submit" class="btn btn-secondary">
137                 {this.state.loginLoading ? (
138                   <svg class="icon icon-spinner spin">
139                     <use xlinkHref="#icon-spinner"></use>
140                   </svg>
141                 ) : (
142                   i18n.t('login')
143                 )}
144               </button>
145             </div>
146           </div>
147         </form>
148       </div>
149     );
150   }
151   registerForm() {
152     return (
153       <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
154         <h5>{i18n.t('sign_up')}</h5>
155
156         <div class="form-group row">
157           <label class="col-sm-2 col-form-label" htmlFor="register-username">
158             {i18n.t('username')}
159           </label>
160
161           <div class="col-sm-10">
162             <input
163               type="text"
164               id="register-username"
165               class="form-control"
166               value={this.state.registerForm.username}
167               onInput={linkEvent(this, this.handleRegisterUsernameChange)}
168               required
169               minLength={3}
170               maxLength={20}
171               pattern="[a-zA-Z0-9_]+"
172             />
173           </div>
174         </div>
175
176         <div class="form-group row">
177           <label class="col-sm-2 col-form-label" htmlFor="register-email">
178             {i18n.t('email')}
179           </label>
180           <div class="col-sm-10">
181             <input
182               type="email"
183               id="register-email"
184               class="form-control"
185               placeholder={i18n.t('optional')}
186               value={this.state.registerForm.email}
187               onInput={linkEvent(this, this.handleRegisterEmailChange)}
188               minLength={3}
189             />
190             {!validEmail(this.state.registerForm.email) && (
191               <div class="mt-2 mb-0 alert alert-light" role="alert">
192                 <svg class="icon icon-inline mr-2">
193                   <use xlinkHref="#icon-alert-triangle"></use>
194                 </svg>
195                 {i18n.t('no_password_reset')}
196               </div>
197             )}
198           </div>
199         </div>
200
201         <div class="form-group row">
202           <label class="col-sm-2 col-form-label" htmlFor="register-password">
203             {i18n.t('password')}
204           </label>
205           <div class="col-sm-10">
206             <input
207               type="password"
208               id="register-password"
209               value={this.state.registerForm.password}
210               autoComplete="new-password"
211               onInput={linkEvent(this, this.handleRegisterPasswordChange)}
212               class="form-control"
213               required
214             />
215           </div>
216         </div>
217
218         <div class="form-group row">
219           <label
220             class="col-sm-2 col-form-label"
221             htmlFor="register-verify-password"
222           >
223             {i18n.t('verify_password')}
224           </label>
225           <div class="col-sm-10">
226             <input
227               type="password"
228               id="register-verify-password"
229               value={this.state.registerForm.password_verify}
230               autoComplete="new-password"
231               onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
232               class="form-control"
233               required
234             />
235           </div>
236         </div>
237         <div class="form-group row">
238           <label class="col-sm-10 col-form-label" htmlFor="register-math">
239             {i18n.t('what_is')}{' '}
240             {`${this.state.mathQuestion.a} + ${this.state.mathQuestion.b}?`}
241           </label>
242
243           <div class="col-sm-2">
244             <input
245               type="number"
246               id="register-math"
247               class="form-control"
248               value={this.state.mathQuestion.answer}
249               onInput={linkEvent(this, this.handleMathAnswerChange)}
250               required
251             />
252           </div>
253         </div>
254         {this.state.enable_nsfw && (
255           <div class="form-group row">
256             <div class="col-sm-10">
257               <div class="form-check">
258                 <input
259                   class="form-check-input"
260                   id="register-show-nsfw"
261                   type="checkbox"
262                   checked={this.state.registerForm.show_nsfw}
263                   onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
264                 />
265                 <label class="form-check-label" htmlFor="register-show-nsfw">
266                   {i18n.t('show_nsfw')}
267                 </label>
268               </div>
269             </div>
270           </div>
271         )}
272         <div class="form-group row">
273           <div class="col-sm-10">
274             <button
275               type="submit"
276               class="btn btn-secondary"
277               disabled={this.mathCheck}
278             >
279               {this.state.registerLoading ? (
280                 <svg class="icon icon-spinner spin">
281                   <use xlinkHref="#icon-spinner"></use>
282                 </svg>
283               ) : (
284                 i18n.t('sign_up')
285               )}
286             </button>
287           </div>
288         </div>
289       </form>
290     );
291   }
292
293   handleLoginSubmit(i: Login, event: any) {
294     event.preventDefault();
295     i.state.loginLoading = true;
296     i.setState(i.state);
297     WebSocketService.Instance.login(i.state.loginForm);
298   }
299
300   handleLoginUsernameChange(i: Login, event: any) {
301     i.state.loginForm.username_or_email = event.target.value;
302     i.setState(i.state);
303   }
304
305   handleLoginPasswordChange(i: Login, event: any) {
306     i.state.loginForm.password = event.target.value;
307     i.setState(i.state);
308   }
309
310   handleRegisterSubmit(i: Login, event: any) {
311     event.preventDefault();
312     i.state.registerLoading = true;
313     i.setState(i.state);
314
315     if (!i.mathCheck) {
316       WebSocketService.Instance.register(i.state.registerForm);
317     }
318   }
319
320   handleRegisterUsernameChange(i: Login, event: any) {
321     i.state.registerForm.username = event.target.value;
322     i.setState(i.state);
323   }
324
325   handleRegisterEmailChange(i: Login, event: any) {
326     i.state.registerForm.email = event.target.value;
327     if (i.state.registerForm.email == '') {
328       i.state.registerForm.email = undefined;
329     }
330     i.setState(i.state);
331   }
332
333   handleRegisterPasswordChange(i: Login, event: any) {
334     i.state.registerForm.password = event.target.value;
335     i.setState(i.state);
336   }
337
338   handleRegisterPasswordVerifyChange(i: Login, event: any) {
339     i.state.registerForm.password_verify = event.target.value;
340     i.setState(i.state);
341   }
342
343   handleRegisterShowNsfwChange(i: Login, event: any) {
344     i.state.registerForm.show_nsfw = event.target.checked;
345     i.setState(i.state);
346   }
347
348   handleMathAnswerChange(i: Login, event: any) {
349     i.state.mathQuestion.answer = event.target.value;
350     i.setState(i.state);
351   }
352
353   handlePasswordReset(i: Login) {
354     event.preventDefault();
355     let resetForm: PasswordResetForm = {
356       email: i.state.loginForm.username_or_email,
357     };
358     WebSocketService.Instance.passwordReset(resetForm);
359   }
360
361   get mathCheck(): boolean {
362     return (
363       this.state.mathQuestion.answer !=
364       this.state.mathQuestion.a + this.state.mathQuestion.b
365     );
366   }
367
368   parseMessage(msg: WebSocketJsonResponse) {
369     let res = wsJsonToRes(msg);
370     if (msg.error) {
371       toast(i18n.t(msg.error), 'danger');
372       this.state = this.emptyState;
373       this.setState(this.state);
374       return;
375     } else {
376       if (res.op == UserOperation.Login) {
377         let data = res.data as LoginResponse;
378         this.state = this.emptyState;
379         this.setState(this.state);
380         UserService.Instance.login(data);
381         WebSocketService.Instance.userJoin();
382         toast(i18n.t('logged_in'));
383         this.props.history.push('/');
384       } else if (res.op == UserOperation.Register) {
385         let data = res.data as LoginResponse;
386         this.state = this.emptyState;
387         this.setState(this.state);
388         UserService.Instance.login(data);
389         WebSocketService.Instance.userJoin();
390         this.props.history.push('/communities');
391       } else if (res.op == UserOperation.PasswordReset) {
392         toast(i18n.t('reset_password_mail_sent'));
393       } else if (res.op == UserOperation.GetSite) {
394         let data = res.data as GetSiteResponse;
395         this.state.enable_nsfw = data.site.enable_nsfw;
396         this.setState(this.state);
397         document.title = `${i18n.t('login')} - ${data.site.name}`;
398       }
399     }
400   }
401 }