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";
14 } from "lemmy-js-client";
15 import { Subscription } from "rxjs";
16 import { i18n } from "../../i18next";
17 import { UserService, WebSocketService } from "../../services";
30 import { HtmlTags } from "../common/html-tags";
31 import { Icon, Spinner } from "../common/icon";
33 const passwordStrengthOptions: Options<string> = [
62 registerForm: Register;
63 loginLoading: boolean;
64 registerLoading: boolean;
65 captcha: GetCaptchaResponse;
66 captchaPlaying: boolean;
70 export class Login extends Component<any, State> {
71 private isoData = setIsoData(this.context);
72 private subscription: Subscription;
73 private audio: HTMLAudioElement;
77 username_or_email: undefined,
83 password_verify: undefined,
85 captcha_uuid: undefined,
86 captcha_answer: undefined,
89 registerLoading: false,
91 captchaPlaying: false,
92 site_view: this.isoData.site_res.site_view,
95 constructor(props: any, context: any) {
96 super(props, context);
98 this.state = this.emptyState;
100 this.parseMessage = this.parseMessage.bind(this);
101 this.subscription = wsSubscribe(this.parseMessage);
104 WebSocketService.Instance.send(wsClient.getCaptcha());
108 componentWillUnmount() {
110 this.subscription.unsubscribe();
114 get documentTitle(): string {
115 return `${i18n.t("login")} - ${this.state.site_view.site.name}`;
118 get isLemmyMl(): boolean {
119 return isBrowser() && window.location.hostname == "lemmy.ml";
124 <div class="container">
126 title={this.documentTitle}
127 path={this.context.router.route.match.url}
130 <div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
131 <div class="col-12 col-lg-6">{this.registerForm()}</div>
140 <form onSubmit={linkEvent(this, this.handleLoginSubmit)}>
141 <h5>{i18n.t("login")}</h5>
142 <div class="form-group row">
144 class="col-sm-2 col-form-label"
145 htmlFor="login-email-or-username"
147 {i18n.t("email_or_username")}
149 <div class="col-sm-10">
153 id="login-email-or-username"
154 value={this.state.loginForm.username_or_email}
155 onInput={linkEvent(this, this.handleLoginUsernameChange)}
162 <div class="form-group row">
163 <label class="col-sm-2 col-form-label" htmlFor="login-password">
166 <div class="col-sm-10">
170 value={this.state.loginForm.password}
171 onInput={linkEvent(this, this.handleLoginPasswordChange)}
173 autoComplete="current-password"
179 onClick={linkEvent(this, this.handlePasswordReset)}
180 className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold pointer-events not-allowed"
181 disabled={!validEmail(this.state.loginForm.username_or_email)}
182 title={i18n.t("no_password_reset")}
184 {i18n.t("forgot_password")}
188 <div class="form-group row">
189 <div class="col-sm-10">
190 <button type="submit" class="btn btn-secondary">
191 {this.state.loginLoading ? <Spinner /> : i18n.t("login")}
202 <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
203 <h5>{i18n.t("sign_up")}</h5>
205 <div class="form-group row">
206 <label class="col-sm-2 col-form-label" htmlFor="register-username">
210 <div class="col-sm-10">
213 id="register-username"
215 value={this.state.registerForm.username}
216 onInput={linkEvent(this, this.handleRegisterUsernameChange)}
219 pattern="[a-zA-Z0-9_]+"
220 title={i18n.t("community_reqs")}
225 <div class="form-group row">
226 <label class="col-sm-2 col-form-label" htmlFor="register-email">
229 <div class="col-sm-10">
234 placeholder={i18n.t("optional")}
235 value={this.state.registerForm.email}
237 onInput={linkEvent(this, this.handleRegisterEmailChange)}
240 {!validEmail(this.state.registerForm.email) && (
241 <div class="mt-2 mb-0 alert alert-light" role="alert">
242 <Icon icon="alert-triangle" classes="icon-inline mr-2" />
243 {i18n.t("no_password_reset")}
249 <div class="form-group row">
250 <label class="col-sm-2 col-form-label" htmlFor="register-password">
253 <div class="col-sm-10">
256 id="register-password"
257 value={this.state.registerForm.password}
258 autoComplete="new-password"
259 onInput={linkEvent(this, this.handleRegisterPasswordChange)}
265 {this.state.registerForm.password && (
266 <div class={this.passwordColorClass}>
267 {i18n.t(this.passwordStrength as I18nKeys)}
273 <div class="form-group row">
275 class="col-sm-2 col-form-label"
276 htmlFor="register-verify-password"
278 {i18n.t("verify_password")}
280 <div class="col-sm-10">
283 id="register-verify-password"
284 value={this.state.registerForm.password_verify}
285 autoComplete="new-password"
286 onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
294 {this.state.captcha && (
295 <div class="form-group row">
296 <label class="col-sm-2" htmlFor="register-captcha">
297 <span class="mr-2">{i18n.t("enter_code")}</span>
300 class="btn btn-secondary"
301 onClick={linkEvent(this, this.handleRegenCaptcha)}
302 aria-label={i18n.t("captcha")}
304 <Icon icon="refresh-cw" classes="icon-refresh-cw" />
308 <div class="col-sm-6">
312 id="register-captcha"
313 value={this.state.registerForm.captcha_answer}
316 this.handleRegisterCaptchaAnswerChange
323 {this.state.site_view.site.enable_nsfw && (
324 <div class="form-group row">
325 <div class="col-sm-10">
326 <div class="form-check">
328 class="form-check-input"
329 id="register-show-nsfw"
331 checked={this.state.registerForm.show_nsfw}
332 onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
334 <label class="form-check-label" htmlFor="register-show-nsfw">
335 {i18n.t("show_nsfw")}
342 <div class="mt-2 mb-0 alert alert-light" role="alert">
343 <T i18nKey="lemmy_ml_registration_message">
344 #<a href={joinLemmyUrl}>#</a>
348 <div class="form-group row">
349 <div class="col-sm-10">
350 <button type="submit" class="btn btn-secondary">
351 {this.state.registerLoading ? <Spinner /> : i18n.t("sign_up")}
361 <div class="col-sm-4">
362 {this.state.captcha.ok && (
365 class="rounded-top img-fluid"
366 src={this.captchaPngSrc()}
367 style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
368 alt={i18n.t("captcha")}
370 {this.state.captcha.ok.wav && (
372 class="rounded-bottom btn btn-sm btn-secondary btn-block"
373 style="border-top-right-radius: 0; border-top-left-radius: 0;"
374 title={i18n.t("play_captcha_audio")}
375 onClick={linkEvent(this, this.handleCaptchaPlay)}
377 disabled={this.state.captchaPlaying}
379 <Icon icon="play" classes="icon-play" />
388 get passwordStrength() {
389 return passwordStrength(
390 this.state.registerForm.password,
391 passwordStrengthOptions
395 get passwordColorClass(): string {
396 let strength = this.passwordStrength;
398 if (["weak", "medium"].includes(strength)) {
399 return "text-warning";
400 } else if (strength == "strong") {
401 return "text-success";
403 return "text-danger";
407 handleLoginSubmit(i: Login, event: any) {
408 event.preventDefault();
409 i.state.loginLoading = true;
411 WebSocketService.Instance.send(wsClient.login(i.state.loginForm));
414 handleLoginUsernameChange(i: Login, event: any) {
415 i.state.loginForm.username_or_email = event.target.value;
419 handleLoginPasswordChange(i: Login, event: any) {
420 i.state.loginForm.password = event.target.value;
424 handleRegisterSubmit(i: Login, event: any) {
425 event.preventDefault();
426 i.state.registerLoading = true;
428 WebSocketService.Instance.send(wsClient.register(i.state.registerForm));
431 handleRegisterUsernameChange(i: Login, event: any) {
432 i.state.registerForm.username = event.target.value;
436 handleRegisterEmailChange(i: Login, event: any) {
437 i.state.registerForm.email = event.target.value;
438 if (i.state.registerForm.email == "") {
439 i.state.registerForm.email = undefined;
444 handleRegisterPasswordChange(i: Login, event: any) {
445 i.state.registerForm.password = event.target.value;
449 handleRegisterPasswordVerifyChange(i: Login, event: any) {
450 i.state.registerForm.password_verify = event.target.value;
454 handleRegisterShowNsfwChange(i: Login, event: any) {
455 i.state.registerForm.show_nsfw = event.target.checked;
459 handleRegisterCaptchaAnswerChange(i: Login, event: any) {
460 i.state.registerForm.captcha_answer = event.target.value;
464 handleRegenCaptcha(i: Login) {
466 i.state.captchaPlaying = false;
468 WebSocketService.Instance.send(wsClient.getCaptcha());
471 handlePasswordReset(i: Login, event: any) {
472 event.preventDefault();
473 let resetForm: PasswordReset = {
474 email: i.state.loginForm.username_or_email,
476 WebSocketService.Instance.send(wsClient.passwordReset(resetForm));
479 handleCaptchaPlay(i: Login) {
480 // This was a bad bug, it should only build the new audio on a new file.
481 // Replays would stop prematurely if this was rebuilt every time.
482 if (i.audio == null) {
483 let base64 = `data:audio/wav;base64,${i.state.captcha.ok.wav}`;
484 i.audio = new Audio(base64);
489 i.state.captchaPlaying = true;
492 i.audio.addEventListener("ended", () => {
493 i.audio.currentTime = 0;
494 i.state.captchaPlaying = false;
500 return `data:image/png;base64,${this.state.captcha.ok.png}`;
503 parseMessage(msg: any) {
504 let op = wsUserOp(msg);
507 toast(i18n.t(msg.error), "danger");
508 this.state = this.emptyState;
509 this.state.registerForm.captcha_answer = undefined;
510 // Refetch another captcha
511 WebSocketService.Instance.send(wsClient.getCaptcha());
512 this.setState(this.state);
515 if (op == UserOperation.Login) {
516 let data = wsJsonToRes<LoginResponse>(msg).data;
517 this.state = this.emptyState;
518 this.setState(this.state);
519 UserService.Instance.login(data);
520 WebSocketService.Instance.send(
525 toast(i18n.t("logged_in"));
526 this.props.history.push("/");
527 } else if (op == UserOperation.Register) {
528 let data = wsJsonToRes<LoginResponse>(msg).data;
529 this.state = this.emptyState;
530 this.setState(this.state);
531 UserService.Instance.login(data);
532 WebSocketService.Instance.send(
537 this.props.history.push("/communities");
538 } else if (op == UserOperation.GetCaptcha) {
539 let data = wsJsonToRes<GetCaptchaResponse>(msg).data;
541 this.state.captcha = data;
542 this.state.registerForm.captcha_uuid = data.ok.uuid;
543 this.setState(this.state);
545 } else if (op == UserOperation.PasswordReset) {
546 toast(i18n.t("reset_password_mail_sent"));
547 } else if (op == UserOperation.GetSite) {
548 let data = wsJsonToRes<GetSiteResponse>(msg).data;
549 this.state.site_view = data.site_view;
550 this.setState(this.state);