]> Untitled Git - lemmy-ui.git/blob - src/shared/components/home/signup.tsx
Private instances (#523)
[lemmy-ui.git] / src / shared / components / home / signup.tsx
1 import { Options, passwordStrength } from "check-password-strength";
2 import { I18nKeys } from "i18next";
3 import { Component, linkEvent } from "inferno";
4 import { T } from "inferno-i18next-dess";
5 import {
6   GetCaptchaResponse,
7   GetSiteResponse,
8   LoginResponse,
9   Register,
10   SiteView,
11   UserOperation,
12 } from "lemmy-js-client";
13 import { Subscription } from "rxjs";
14 import { i18n } from "../../i18next";
15 import { UserService, WebSocketService } from "../../services";
16 import {
17   authField,
18   isBrowser,
19   joinLemmyUrl,
20   mdToHtml,
21   setIsoData,
22   toast,
23   validEmail,
24   wsClient,
25   wsJsonToRes,
26   wsSubscribe,
27   wsUserOp,
28 } from "../../utils";
29 import { HtmlTags } from "../common/html-tags";
30 import { Icon, Spinner } from "../common/icon";
31 import { MarkdownTextArea } from "../common/markdown-textarea";
32
33 const passwordStrengthOptions: Options<string> = [
34   {
35     id: 0,
36     value: "very_weak",
37     minDiversity: 0,
38     minLength: 0,
39   },
40   {
41     id: 1,
42     value: "weak",
43     minDiversity: 2,
44     minLength: 10,
45   },
46   {
47     id: 2,
48     value: "medium",
49     minDiversity: 3,
50     minLength: 12,
51   },
52   {
53     id: 3,
54     value: "strong",
55     minDiversity: 4,
56     minLength: 14,
57   },
58 ];
59
60 interface State {
61   registerForm: Register;
62   registerLoading: boolean;
63   captcha: GetCaptchaResponse;
64   captchaPlaying: boolean;
65   site_view: SiteView;
66 }
67
68 export class Signup extends Component<any, State> {
69   private isoData = setIsoData(this.context);
70   private subscription: Subscription;
71   private audio: HTMLAudioElement;
72
73   emptyState: State = {
74     registerForm: {
75       username: undefined,
76       password: undefined,
77       password_verify: undefined,
78       show_nsfw: false,
79       captcha_uuid: undefined,
80       captcha_answer: undefined,
81       honeypot: undefined,
82       answer: undefined,
83     },
84     registerLoading: false,
85     captcha: undefined,
86     captchaPlaying: false,
87     site_view: this.isoData.site_res.site_view,
88   };
89
90   constructor(props: any, context: any) {
91     super(props, context);
92
93     this.state = this.emptyState;
94     this.handleAnswerChange = this.handleAnswerChange.bind(this);
95
96     this.parseMessage = this.parseMessage.bind(this);
97     this.subscription = wsSubscribe(this.parseMessage);
98
99     if (isBrowser()) {
100       WebSocketService.Instance.send(wsClient.getCaptcha());
101     }
102   }
103
104   componentWillUnmount() {
105     if (isBrowser()) {
106       this.subscription.unsubscribe();
107     }
108   }
109
110   get documentTitle(): string {
111     return `${this.titleName} - ${this.state.site_view.site.name}`;
112   }
113
114   get titleName(): string {
115     return `${i18n.t(
116       this.state.site_view.site.private_instance ? "apply_to_join" : "sign_up"
117     )}`;
118   }
119
120   get isLemmyMl(): boolean {
121     return isBrowser() && window.location.hostname == "lemmy.ml";
122   }
123
124   render() {
125     return (
126       <div class="container">
127         <HtmlTags
128           title={this.documentTitle}
129           path={this.context.router.route.match.url}
130         />
131         <div class="row">
132           <div class="col-12 col-lg-6 offset-lg-3">{this.registerForm()}</div>
133         </div>
134       </div>
135     );
136   }
137
138   registerForm() {
139     return (
140       <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
141         <h5>{this.titleName}</h5>
142
143         <div class="form-group row">
144           <label class="col-sm-2 col-form-label" htmlFor="register-username">
145             {i18n.t("username")}
146           </label>
147
148           <div class="col-sm-10">
149             <input
150               type="text"
151               id="register-username"
152               class="form-control"
153               value={this.state.registerForm.username}
154               onInput={linkEvent(this, this.handleRegisterUsernameChange)}
155               required
156               minLength={3}
157               pattern="[a-z0-9_]+"
158               title={i18n.t("community_reqs")}
159             />
160           </div>
161         </div>
162
163         <div class="form-group row">
164           <label class="col-sm-2 col-form-label" htmlFor="register-email">
165             {i18n.t("email")}
166           </label>
167           <div class="col-sm-10">
168             <input
169               type="email"
170               id="register-email"
171               class="form-control"
172               placeholder={
173                 this.state.site_view.site.require_email_verification
174                   ? i18n.t("required")
175                   : i18n.t("optional")
176               }
177               value={this.state.registerForm.email}
178               autoComplete="email"
179               onInput={linkEvent(this, this.handleRegisterEmailChange)}
180               required={this.state.site_view.site.require_email_verification}
181               minLength={3}
182             />
183             {!this.state.site_view.site.require_email_verification &&
184               !validEmail(this.state.registerForm.email) && (
185                 <div class="mt-2 mb-0 alert alert-light" role="alert">
186                   <Icon icon="alert-triangle" classes="icon-inline mr-2" />
187                   {i18n.t("no_password_reset")}
188                 </div>
189               )}
190           </div>
191         </div>
192
193         <div class="form-group row">
194           <label class="col-sm-2 col-form-label" htmlFor="register-password">
195             {i18n.t("password")}
196           </label>
197           <div class="col-sm-10">
198             <input
199               type="password"
200               id="register-password"
201               value={this.state.registerForm.password}
202               autoComplete="new-password"
203               onInput={linkEvent(this, this.handleRegisterPasswordChange)}
204               minLength={10}
205               maxLength={60}
206               class="form-control"
207               required
208             />
209             {this.state.registerForm.password && (
210               <div class={this.passwordColorClass}>
211                 {i18n.t(this.passwordStrength as I18nKeys)}
212               </div>
213             )}
214           </div>
215         </div>
216
217         <div class="form-group row">
218           <label
219             class="col-sm-2 col-form-label"
220             htmlFor="register-verify-password"
221           >
222             {i18n.t("verify_password")}
223           </label>
224           <div class="col-sm-10">
225             <input
226               type="password"
227               id="register-verify-password"
228               value={this.state.registerForm.password_verify}
229               autoComplete="new-password"
230               onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
231               maxLength={60}
232               class="form-control"
233               required
234             />
235           </div>
236         </div>
237
238         {this.state.site_view.site.require_application && (
239           <>
240             <div class="form-group row">
241               <div class="offset-sm-2 col-sm-10">
242                 <div class="mt-2 alert alert-light" role="alert">
243                   <Icon icon="alert-triangle" classes="icon-inline mr-2" />
244                   {i18n.t("fill_out_application")}
245                 </div>
246                 <div
247                   className="md-div"
248                   dangerouslySetInnerHTML={mdToHtml(
249                     this.state.site_view.site.application_question || ""
250                   )}
251                 />
252               </div>
253             </div>
254
255             <div class="form-group row">
256               <label
257                 class="col-sm-2 col-form-label"
258                 htmlFor="application_answer"
259               >
260                 {i18n.t("answer")}
261               </label>
262               <div class="col-sm-10">
263                 <MarkdownTextArea
264                   onContentChange={this.handleAnswerChange}
265                   hideNavigationWarnings
266                 />
267               </div>
268             </div>
269           </>
270         )}
271
272         {this.state.captcha && (
273           <div class="form-group row">
274             <label class="col-sm-2" htmlFor="register-captcha">
275               <span class="mr-2">{i18n.t("enter_code")}</span>
276               <button
277                 type="button"
278                 class="btn btn-secondary"
279                 onClick={linkEvent(this, this.handleRegenCaptcha)}
280                 aria-label={i18n.t("captcha")}
281               >
282                 <Icon icon="refresh-cw" classes="icon-refresh-cw" />
283               </button>
284             </label>
285             {this.showCaptcha()}
286             <div class="col-sm-6">
287               <input
288                 type="text"
289                 class="form-control"
290                 id="register-captcha"
291                 value={this.state.registerForm.captcha_answer}
292                 onInput={linkEvent(
293                   this,
294                   this.handleRegisterCaptchaAnswerChange
295                 )}
296                 required
297               />
298             </div>
299           </div>
300         )}
301         {this.state.site_view.site.enable_nsfw && (
302           <div class="form-group row">
303             <div class="col-sm-10">
304               <div class="form-check">
305                 <input
306                   class="form-check-input"
307                   id="register-show-nsfw"
308                   type="checkbox"
309                   checked={this.state.registerForm.show_nsfw}
310                   onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
311                 />
312                 <label class="form-check-label" htmlFor="register-show-nsfw">
313                   {i18n.t("show_nsfw")}
314                 </label>
315               </div>
316             </div>
317           </div>
318         )}
319         {this.isLemmyMl && (
320           <div class="mt-2 mb-0 alert alert-light" role="alert">
321             <T i18nKey="lemmy_ml_registration_message">
322               #<a href={joinLemmyUrl}>#</a>
323             </T>
324           </div>
325         )}
326         <input
327           tabIndex={-1}
328           autoComplete="false"
329           name="a_password"
330           type="text"
331           class="form-control honeypot"
332           id="register-honey"
333           value={this.state.registerForm.honeypot}
334           onInput={linkEvent(this, this.handleHoneyPotChange)}
335         />
336         <div class="form-group row">
337           <div class="col-sm-10">
338             <button type="submit" class="btn btn-secondary">
339               {this.state.registerLoading ? <Spinner /> : this.titleName}
340             </button>
341           </div>
342         </div>
343       </form>
344     );
345   }
346
347   showCaptcha() {
348     return (
349       <div class="col-sm-4">
350         {this.state.captcha.ok && (
351           <>
352             <img
353               class="rounded-top img-fluid"
354               src={this.captchaPngSrc()}
355               style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
356               alt={i18n.t("captcha")}
357             />
358             {this.state.captcha.ok.wav && (
359               <button
360                 class="rounded-bottom btn btn-sm btn-secondary btn-block"
361                 style="border-top-right-radius: 0; border-top-left-radius: 0;"
362                 title={i18n.t("play_captcha_audio")}
363                 onClick={linkEvent(this, this.handleCaptchaPlay)}
364                 type="button"
365                 disabled={this.state.captchaPlaying}
366               >
367                 <Icon icon="play" classes="icon-play" />
368               </button>
369             )}
370           </>
371         )}
372       </div>
373     );
374   }
375
376   get passwordStrength() {
377     return passwordStrength(
378       this.state.registerForm.password,
379       passwordStrengthOptions
380     ).value;
381   }
382
383   get passwordColorClass(): string {
384     let strength = this.passwordStrength;
385
386     if (["weak", "medium"].includes(strength)) {
387       return "text-warning";
388     } else if (strength == "strong") {
389       return "text-success";
390     } else {
391       return "text-danger";
392     }
393   }
394
395   handleRegisterSubmit(i: Signup, event: any) {
396     event.preventDefault();
397     i.state.registerLoading = true;
398     i.setState(i.state);
399     WebSocketService.Instance.send(wsClient.register(i.state.registerForm));
400   }
401
402   handleRegisterUsernameChange(i: Signup, event: any) {
403     i.state.registerForm.username = event.target.value;
404     i.setState(i.state);
405   }
406
407   handleRegisterEmailChange(i: Signup, event: any) {
408     i.state.registerForm.email = event.target.value;
409     if (i.state.registerForm.email == "") {
410       i.state.registerForm.email = undefined;
411     }
412     i.setState(i.state);
413   }
414
415   handleRegisterPasswordChange(i: Signup, event: any) {
416     i.state.registerForm.password = event.target.value;
417     i.setState(i.state);
418   }
419
420   handleRegisterPasswordVerifyChange(i: Signup, event: any) {
421     i.state.registerForm.password_verify = event.target.value;
422     i.setState(i.state);
423   }
424
425   handleRegisterShowNsfwChange(i: Signup, event: any) {
426     i.state.registerForm.show_nsfw = event.target.checked;
427     i.setState(i.state);
428   }
429
430   handleRegisterCaptchaAnswerChange(i: Signup, event: any) {
431     i.state.registerForm.captcha_answer = event.target.value;
432     i.setState(i.state);
433   }
434
435   handleAnswerChange(val: string) {
436     this.state.registerForm.answer = val;
437     this.setState(this.state);
438   }
439
440   handleHoneyPotChange(i: Signup, event: any) {
441     i.state.registerForm.honeypot = event.target.value;
442     i.setState(i.state);
443   }
444
445   handleRegenCaptcha(i: Signup) {
446     i.audio = null;
447     i.state.captchaPlaying = false;
448     i.setState(i.state);
449     WebSocketService.Instance.send(wsClient.getCaptcha());
450   }
451
452   handleCaptchaPlay(i: Signup) {
453     // This was a bad bug, it should only build the new audio on a new file.
454     // Replays would stop prematurely if this was rebuilt every time.
455     if (i.audio == null) {
456       let base64 = `data:audio/wav;base64,${i.state.captcha.ok.wav}`;
457       i.audio = new Audio(base64);
458     }
459
460     i.audio.play();
461
462     i.state.captchaPlaying = true;
463     i.setState(i.state);
464
465     i.audio.addEventListener("ended", () => {
466       i.audio.currentTime = 0;
467       i.state.captchaPlaying = false;
468       i.setState(i.state);
469     });
470   }
471
472   captchaPngSrc() {
473     return `data:image/png;base64,${this.state.captcha.ok.png}`;
474   }
475
476   parseMessage(msg: any) {
477     let op = wsUserOp(msg);
478     console.log(msg);
479     if (msg.error) {
480       toast(i18n.t(msg.error), "danger");
481       this.state = this.emptyState;
482       this.state.registerForm.captcha_answer = undefined;
483       // Refetch another captcha
484       // WebSocketService.Instance.send(wsClient.getCaptcha());
485       this.setState(this.state);
486       return;
487     } else {
488       if (op == UserOperation.Register) {
489         let data = wsJsonToRes<LoginResponse>(msg).data;
490         this.state = this.emptyState;
491         this.setState(this.state);
492         // Only log them in if a jwt was set
493         if (data.jwt) {
494           UserService.Instance.login(data);
495           WebSocketService.Instance.send(
496             wsClient.userJoin({
497               auth: authField(),
498             })
499           );
500           this.props.history.push("/communities");
501         } else {
502           if (data.verify_email_sent) {
503             toast(i18n.t("verify_email_sent"));
504           }
505           if (data.registration_created) {
506             toast(i18n.t("registration_application_sent"));
507           }
508           this.props.history.push("/");
509         }
510       } else if (op == UserOperation.GetCaptcha) {
511         let data = wsJsonToRes<GetCaptchaResponse>(msg).data;
512         if (data.ok) {
513           this.state.captcha = data;
514           this.state.registerForm.captcha_uuid = data.ok.uuid;
515           this.setState(this.state);
516         }
517       } else if (op == UserOperation.PasswordReset) {
518         toast(i18n.t("reset_password_mail_sent"));
519       } else if (op == UserOperation.GetSite) {
520         let data = wsJsonToRes<GetSiteResponse>(msg).data;
521         this.state.site_view = data.site_view;
522         this.setState(this.state);
523       }
524     }
525   }
526 }