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_]+"
224 <div class="form-group row">
225 <label class="col-sm-2 col-form-label" htmlFor="register-email">
228 <div class="col-sm-10">
233 placeholder={i18n.t("optional")}
234 value={this.state.registerForm.email}
236 onInput={linkEvent(this, this.handleRegisterEmailChange)}
239 {!validEmail(this.state.registerForm.email) && (
240 <div class="mt-2 mb-0 alert alert-light" role="alert">
241 <Icon icon="alert-triangle" classes="icon-inline mr-2" />
242 {i18n.t("no_password_reset")}
248 <div class="form-group row">
249 <label class="col-sm-2 col-form-label" htmlFor="register-password">
252 <div class="col-sm-10">
255 id="register-password"
256 value={this.state.registerForm.password}
257 autoComplete="new-password"
258 onInput={linkEvent(this, this.handleRegisterPasswordChange)}
264 {this.state.registerForm.password && (
265 <div class={this.passwordColorClass}>
266 {i18n.t(this.passwordStrength as I18nKeys)}
272 <div class="form-group row">
274 class="col-sm-2 col-form-label"
275 htmlFor="register-verify-password"
277 {i18n.t("verify_password")}
279 <div class="col-sm-10">
282 id="register-verify-password"
283 value={this.state.registerForm.password_verify}
284 autoComplete="new-password"
285 onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)}
293 {this.state.captcha && (
294 <div class="form-group row">
295 <label class="col-sm-2" htmlFor="register-captcha">
296 <span class="mr-2">{i18n.t("enter_code")}</span>
299 class="btn btn-secondary"
300 onClick={linkEvent(this, this.handleRegenCaptcha)}
301 aria-label={i18n.t("captcha")}
303 <Icon icon="refresh-cw" classes="icon-refresh-cw" />
307 <div class="col-sm-6">
311 id="register-captcha"
312 value={this.state.registerForm.captcha_answer}
315 this.handleRegisterCaptchaAnswerChange
322 {this.state.site_view.site.enable_nsfw && (
323 <div class="form-group row">
324 <div class="col-sm-10">
325 <div class="form-check">
327 class="form-check-input"
328 id="register-show-nsfw"
330 checked={this.state.registerForm.show_nsfw}
331 onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
333 <label class="form-check-label" htmlFor="register-show-nsfw">
334 {i18n.t("show_nsfw")}
341 <div class="mt-2 mb-0 alert alert-light" role="alert">
342 <T i18nKey="lemmy_ml_registration_message">
343 #<a href={joinLemmyUrl}>#</a>
347 <div class="form-group row">
348 <div class="col-sm-10">
349 <button type="submit" class="btn btn-secondary">
350 {this.state.registerLoading ? <Spinner /> : i18n.t("sign_up")}
360 <div class="col-sm-4">
361 {this.state.captcha.ok && (
364 class="rounded-top img-fluid"
365 src={this.captchaPngSrc()}
366 style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
367 alt={i18n.t("captcha")}
369 {this.state.captcha.ok.wav && (
371 class="rounded-bottom btn btn-sm btn-secondary btn-block"
372 style="border-top-right-radius: 0; border-top-left-radius: 0;"
373 title={i18n.t("play_captcha_audio")}
374 onClick={linkEvent(this, this.handleCaptchaPlay)}
376 disabled={this.state.captchaPlaying}
378 <Icon icon="play" classes="icon-play" />
387 get passwordStrength() {
388 return passwordStrength(
389 this.state.registerForm.password,
390 passwordStrengthOptions
394 get passwordColorClass(): string {
395 let strength = this.passwordStrength;
397 if (["weak", "medium"].includes(strength)) {
398 return "text-warning";
399 } else if (strength == "strong") {
400 return "text-success";
402 return "text-danger";
406 handleLoginSubmit(i: Login, event: any) {
407 event.preventDefault();
408 i.state.loginLoading = true;
410 WebSocketService.Instance.send(wsClient.login(i.state.loginForm));
413 handleLoginUsernameChange(i: Login, event: any) {
414 i.state.loginForm.username_or_email = event.target.value;
418 handleLoginPasswordChange(i: Login, event: any) {
419 i.state.loginForm.password = event.target.value;
423 handleRegisterSubmit(i: Login, event: any) {
424 event.preventDefault();
425 i.state.registerLoading = true;
427 WebSocketService.Instance.send(wsClient.register(i.state.registerForm));
430 handleRegisterUsernameChange(i: Login, event: any) {
431 i.state.registerForm.username = event.target.value;
435 handleRegisterEmailChange(i: Login, event: any) {
436 i.state.registerForm.email = event.target.value;
437 if (i.state.registerForm.email == "") {
438 i.state.registerForm.email = undefined;
443 handleRegisterPasswordChange(i: Login, event: any) {
444 i.state.registerForm.password = event.target.value;
448 handleRegisterPasswordVerifyChange(i: Login, event: any) {
449 i.state.registerForm.password_verify = event.target.value;
453 handleRegisterShowNsfwChange(i: Login, event: any) {
454 i.state.registerForm.show_nsfw = event.target.checked;
458 handleRegisterCaptchaAnswerChange(i: Login, event: any) {
459 i.state.registerForm.captcha_answer = event.target.value;
463 handleRegenCaptcha(i: Login) {
465 i.state.captchaPlaying = false;
467 WebSocketService.Instance.send(wsClient.getCaptcha());
470 handlePasswordReset(i: Login, event: any) {
471 event.preventDefault();
472 let resetForm: PasswordReset = {
473 email: i.state.loginForm.username_or_email,
475 WebSocketService.Instance.send(wsClient.passwordReset(resetForm));
478 handleCaptchaPlay(i: Login) {
479 // This was a bad bug, it should only build the new audio on a new file.
480 // Replays would stop prematurely if this was rebuilt every time.
481 if (i.audio == null) {
482 let base64 = `data:audio/wav;base64,${i.state.captcha.ok.wav}`;
483 i.audio = new Audio(base64);
488 i.state.captchaPlaying = true;
491 i.audio.addEventListener("ended", () => {
492 i.audio.currentTime = 0;
493 i.state.captchaPlaying = false;
499 return `data:image/png;base64,${this.state.captcha.ok.png}`;
502 parseMessage(msg: any) {
503 let op = wsUserOp(msg);
506 toast(i18n.t(msg.error), "danger");
507 this.state = this.emptyState;
508 this.state.registerForm.captcha_answer = undefined;
509 // Refetch another captcha
510 WebSocketService.Instance.send(wsClient.getCaptcha());
511 this.setState(this.state);
514 if (op == UserOperation.Login) {
515 let data = wsJsonToRes<LoginResponse>(msg).data;
516 this.state = this.emptyState;
517 this.setState(this.state);
518 UserService.Instance.login(data);
519 WebSocketService.Instance.send(
524 toast(i18n.t("logged_in"));
525 this.props.history.push("/");
526 } else if (op == UserOperation.Register) {
527 let data = wsJsonToRes<LoginResponse>(msg).data;
528 this.state = this.emptyState;
529 this.setState(this.state);
530 UserService.Instance.login(data);
531 WebSocketService.Instance.send(
536 this.props.history.push("/communities");
537 } else if (op == UserOperation.GetCaptcha) {
538 let data = wsJsonToRes<GetCaptchaResponse>(msg).data;
540 this.state.captcha = data;
541 this.state.registerForm.captcha_uuid = data.ok.uuid;
542 this.setState(this.state);
544 } else if (op == UserOperation.PasswordReset) {
545 toast(i18n.t("reset_password_mail_sent"));
546 } else if (op == UserOperation.GetSite) {
547 let data = wsJsonToRes<GetSiteResponse>(msg).data;
548 this.state.site_view = data.site_view;
549 this.setState(this.state);