1 import { Options, passwordStrength } from "check-password-strength";
2 import { NoOptionI18nKeys } from "i18next";
3 import { Component, linkEvent } from "inferno";
4 import { T } from "inferno-i18next-dess";
15 } from "lemmy-js-client";
16 import { Subscription } from "rxjs";
17 import { i18n } from "../../i18next";
18 import { UserService, WebSocketService } from "../../services";
29 import { HtmlTags } from "../common/html-tags";
30 import { Icon, Spinner } from "../common/icon";
31 import { MarkdownTextArea } from "../common/markdown-textarea";
33 const passwordStrengthOptions: Options<string> = [
65 password_verify?: string;
67 captcha_uuid?: string;
68 captcha_answer?: string;
72 registerLoading: boolean;
73 captcha?: GetCaptchaResponse;
74 captchaPlaying: boolean;
75 siteRes: GetSiteResponse;
78 export class Signup extends Component<any, State> {
79 private isoData = setIsoData(this.context);
80 private subscription?: Subscription;
81 private audio?: HTMLAudioElement;
87 registerLoading: false,
88 captchaPlaying: false,
89 siteRes: this.isoData.site_res,
92 constructor(props: any, context: any) {
93 super(props, context);
95 this.handleAnswerChange = this.handleAnswerChange.bind(this);
97 this.parseMessage = this.parseMessage.bind(this);
98 this.subscription = wsSubscribe(this.parseMessage);
101 WebSocketService.Instance.send(wsClient.getCaptcha({}));
105 componentWillUnmount() {
107 this.subscription?.unsubscribe();
111 get documentTitle(): string {
112 const siteView = this.state.siteRes.site_view;
113 return `${this.titleName(siteView)} - ${siteView.site.name}`;
116 titleName(siteView: SiteView): string {
118 siteView.local_site.private_instance ? "apply_to_join" : "sign_up"
122 get isLemmyMl(): boolean {
123 return isBrowser() && window.location.hostname == "lemmy.ml";
128 <div className="container-lg">
130 title={this.documentTitle}
131 path={this.context.router.route.match.url}
133 <div className="row">
134 <div className="col-12 col-lg-6 offset-lg-3">
135 {this.registerForm()}
143 const siteView = this.state.siteRes.site_view;
145 <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
146 <h5>{this.titleName(siteView)}</h5>
149 <div className="form-group row">
150 <div className="mt-2 mb-0 alert alert-warning" role="alert">
151 <T i18nKey="lemmy_ml_registration_message">
152 #<a href={joinLemmyUrl}>#</a>
158 <div className="form-group row">
160 className="col-sm-2 col-form-label"
161 htmlFor="register-username"
166 <div className="col-sm-10">
169 id="register-username"
170 className="form-control"
171 value={this.state.form.username}
172 onInput={linkEvent(this, this.handleRegisterUsernameChange)}
175 pattern="[a-zA-Z0-9_]+"
176 title={i18n.t("community_reqs")}
181 <div className="form-group row">
182 <label className="col-sm-2 col-form-label" htmlFor="register-email">
185 <div className="col-sm-10">
189 className="form-control"
191 siteView.local_site.require_email_verification
195 value={this.state.form.email}
197 onInput={linkEvent(this, this.handleRegisterEmailChange)}
198 required={siteView.local_site.require_email_verification}
201 {!siteView.local_site.require_email_verification &&
202 this.state.form.email &&
203 !validEmail(this.state.form.email) && (
204 <div className="mt-2 mb-0 alert alert-warning" role="alert">
205 <Icon icon="alert-triangle" classes="icon-inline mr-2" />
206 {i18n.t("no_password_reset")}
212 <div className="form-group row">
214 className="col-sm-2 col-form-label"
215 htmlFor="register-password"
219 <div className="col-sm-10">
222 id="register-password"
223 value={this.state.form.password}
224 autoComplete="new-password"
225 onInput={linkEvent(this, this.handleRegisterPasswordChange)}
228 className="form-control"
231 {this.state.form.password && (
232 <div className={this.passwordColorClass}>
233 {i18n.t(this.passwordStrength as NoOptionI18nKeys)}
239 <div className="form-group row">
241 className="col-sm-2 col-form-label"
242 htmlFor="register-verify-password"
244 {i18n.t("verify_password")}
246 <div className="col-sm-10">
249 id="register-verify-password"
250 value={this.state.form.password_verify}
251 autoComplete="new-password"
252 onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
254 className="form-control"
260 {siteView.local_site.registration_mode == "RequireApplication" && (
262 <div className="form-group row">
263 <div className="offset-sm-2 col-sm-10">
264 <div className="mt-2 alert alert-warning" role="alert">
265 <Icon icon="alert-triangle" classes="icon-inline mr-2" />
266 {i18n.t("fill_out_application")}
268 {siteView.local_site.application_question && (
271 dangerouslySetInnerHTML={mdToHtml(
272 siteView.local_site.application_question
279 <div className="form-group row">
281 className="col-sm-2 col-form-label"
282 htmlFor="application_answer"
286 <div className="col-sm-10">
288 onContentChange={this.handleAnswerChange}
289 hideNavigationWarnings
298 {this.state.captcha && (
299 <div className="form-group row">
300 <label className="col-sm-2" htmlFor="register-captcha">
301 <span className="mr-2">{i18n.t("enter_code")}</span>
304 className="btn btn-secondary"
305 onClick={linkEvent(this, this.handleRegenCaptcha)}
306 aria-label={i18n.t("captcha")}
308 <Icon icon="refresh-cw" classes="icon-refresh-cw" />
312 <div className="col-sm-6">
315 className="form-control"
316 id="register-captcha"
317 value={this.state.form.captcha_answer}
320 this.handleRegisterCaptchaAnswerChange
327 {siteView.local_site.enable_nsfw && (
328 <div className="form-group row">
329 <div className="col-sm-10">
330 <div className="form-check">
332 className="form-check-input"
333 id="register-show-nsfw"
335 checked={this.state.form.show_nsfw}
336 onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
339 className="form-check-label"
340 htmlFor="register-show-nsfw"
342 {i18n.t("show_nsfw")}
353 className="form-control honeypot"
355 value={this.state.form.honeypot}
356 onInput={linkEvent(this, this.handleHoneyPotChange)}
358 <div className="form-group row">
359 <div className="col-sm-10">
360 <button type="submit" className="btn btn-secondary">
361 {this.state.registerLoading ? (
364 this.titleName(siteView)
374 const captchaRes = this.state.captcha?.ok;
375 return captchaRes ? (
376 <div className="col-sm-4">
379 className="rounded-top img-fluid"
380 src={this.captchaPngSrc(captchaRes)}
381 style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
382 alt={i18n.t("captcha")}
386 className="rounded-bottom btn btn-sm btn-secondary btn-block"
387 style="border-top-right-radius: 0; border-top-left-radius: 0;"
388 title={i18n.t("play_captcha_audio")}
389 onClick={linkEvent(this, this.handleCaptchaPlay)}
391 disabled={this.state.captchaPlaying}
393 <Icon icon="play" classes="icon-play" />
403 get passwordStrength(): string | undefined {
404 const password = this.state.form.password;
406 ? passwordStrength(password, passwordStrengthOptions).value
410 get passwordColorClass(): string {
411 const strength = this.passwordStrength;
413 if (strength && ["weak", "medium"].includes(strength)) {
414 return "text-warning";
415 } else if (strength == "strong") {
416 return "text-success";
418 return "text-danger";
422 handleRegisterSubmit(i: Signup, event: any) {
423 event.preventDefault();
424 i.setState({ registerLoading: true });
425 const cForm = i.state.form;
426 if (cForm.username && cForm.password && cForm.password_verify) {
427 const form: Register = {
428 username: cForm.username,
429 password: cForm.password,
430 password_verify: cForm.password_verify,
432 show_nsfw: cForm.show_nsfw,
433 captcha_uuid: cForm.captcha_uuid,
434 captcha_answer: cForm.captcha_answer,
435 honeypot: cForm.honeypot,
436 answer: cForm.answer,
438 WebSocketService.Instance.send(wsClient.register(form));
442 handleRegisterUsernameChange(i: Signup, event: any) {
443 i.state.form.username = event.target.value;
447 handleRegisterEmailChange(i: Signup, event: any) {
448 i.state.form.email = event.target.value;
449 if (i.state.form.email == "") {
450 i.state.form.email = undefined;
455 handleRegisterPasswordChange(i: Signup, event: any) {
456 i.state.form.password = event.target.value;
460 handleRegisterPasswordVerifyChange(i: Signup, event: any) {
461 i.state.form.password_verify = event.target.value;
465 handleRegisterShowNsfwChange(i: Signup, event: any) {
466 i.state.form.show_nsfw = event.target.checked;
470 handleRegisterCaptchaAnswerChange(i: Signup, event: any) {
471 i.state.form.captcha_answer = event.target.value;
475 handleAnswerChange(val: string) {
476 this.setState(s => ((s.form.answer = val), s));
479 handleHoneyPotChange(i: Signup, event: any) {
480 i.state.form.honeypot = event.target.value;
484 handleRegenCaptcha(i: Signup) {
486 i.setState({ captchaPlaying: false });
487 WebSocketService.Instance.send(wsClient.getCaptcha({}));
490 handleCaptchaPlay(i: Signup) {
491 // This was a bad bug, it should only build the new audio on a new file.
492 // Replays would stop prematurely if this was rebuilt every time.
493 const captchaRes = i.state.captcha?.ok;
496 const base64 = `data:audio/wav;base64,${captchaRes.wav}`;
497 i.audio = new Audio(base64);
500 i.setState({ captchaPlaying: true });
502 i.audio.addEventListener("ended", () => {
504 i.audio.currentTime = 0;
505 i.setState({ captchaPlaying: false });
512 captchaPngSrc(captcha: CaptchaResponse) {
513 return `data:image/png;base64,${captcha.png}`;
516 parseMessage(msg: any) {
517 const op = wsUserOp(msg);
520 toast(i18n.t(msg.error), "danger");
521 this.setState(s => ((s.form.captcha_answer = undefined), s));
522 // Refetch another captcha
523 // WebSocketService.Instance.send(wsClient.getCaptcha());
526 if (op == UserOperation.Register) {
527 const data = wsJsonToRes<LoginResponse>(msg);
528 // Only log them in if a jwt was set
530 UserService.Instance.login(data);
531 this.props.history.push("/communities");
534 if (data.verify_email_sent) {
535 toast(i18n.t("verify_email_sent"));
537 if (data.registration_created) {
538 toast(i18n.t("registration_application_sent"));
540 this.props.history.push("/");
542 } else if (op == UserOperation.GetCaptcha) {
543 const data = wsJsonToRes<GetCaptchaResponse>(msg);
545 this.setState({ captcha: data });
546 this.setState(s => ((s.form.captcha_uuid = data.ok?.uuid), s));
548 } else if (op == UserOperation.PasswordReset) {
549 toast(i18n.t("reset_password_mail_sent"));
550 } else if (op == UserOperation.GetSite) {
551 const data = wsJsonToRes<GetSiteResponse>(msg);
552 this.setState({ siteRes: data });