"inferno-server": "^8.0.6",
"isomorphic-cookie": "^1.2.4",
"jwt-decode": "^3.1.2",
- "lemmy-js-client": "0.17.2-rc.1",
+ "lemmy-js-client": "0.17.2-rc.3",
"markdown-it": "^13.0.1",
"markdown-it-container": "^3.0.0",
"markdown-it-footnote": "^3.0.3",
form: {
username_or_email?: string;
password?: string;
+ totp_2fa_token?: string;
};
loginLoading: boolean;
+ showTotp: boolean;
siteRes: GetSiteResponse;
}
state: State = {
form: {},
loginLoading: false,
+ showTotp: false,
siteRes: this.isoData.site_res,
};
</button>
</div>
</div>
+ {this.state.showTotp && (
+ <div className="form-group row">
+ <label
+ className="col-sm-6 col-form-label"
+ htmlFor="login-totp-token"
+ >
+ {i18n.t("two_factor_token")}
+ </label>
+ <div className="col-sm-6">
+ <input
+ type="number"
+ inputMode="numeric"
+ className="form-control"
+ id="login-totp-token"
+ pattern="[0-9]*"
+ autoComplete="one-time-code"
+ value={this.state.form.totp_2fa_token}
+ onInput={linkEvent(this, this.handleLoginTotpChange)}
+ />
+ </div>
+ </div>
+ )}
<div className="form-group row">
<div className="col-sm-10">
<button type="submit" className="btn btn-secondary">
let lForm = i.state.form;
let username_or_email = lForm.username_or_email;
let password = lForm.password;
+ let totp_2fa_token = lForm.totp_2fa_token;
if (username_or_email && password) {
let form: LoginI = {
username_or_email,
password,
+ totp_2fa_token,
};
WebSocketService.Instance.send(wsClient.login(form));
}
i.setState(i.state);
}
+ handleLoginTotpChange(i: Login, event: any) {
+ i.state.form.totp_2fa_token = event.target.value;
+ i.setState(i.state);
+ }
+
handleLoginPasswordChange(i: Login, event: any) {
i.state.form.password = event.target.value;
i.setState(i.state);
let op = wsUserOp(msg);
console.log(msg);
if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- this.setState({ form: {} });
- return;
+ // If the error comes back that the token is missing, show the TOTP field
+ if (msg.error == "missing_totp_token") {
+ this.setState({ showTotp: true, loginLoading: false });
+ toast(i18n.t("enter_two_factor_code"));
+ return;
+ } else {
+ toast(i18n.t(msg.error), "danger");
+ this.setState({ form: {}, loginLoading: false });
+ return;
+ }
} else {
if (op == UserOperation.Login) {
let data = wsJsonToRes<LoginResponse>(msg);
show_read_posts?: boolean;
show_new_post_notifs?: boolean;
discussion_languages?: number[];
+ generate_totp_2fa?: boolean;
};
changePasswordForm: {
new_password?: string;
</label>
</div>
</div>
+ {this.totpSection()}
<div className="form-group">
<button type="submit" className="btn btn-block btn-secondary mr-4">
{this.state.saveUserSettingsLoading ? (
);
}
+ totpSection() {
+ let totpUrl =
+ UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url;
+
+ return (
+ <>
+ {!totpUrl && (
+ <div className="form-group">
+ <div className="form-check">
+ <input
+ className="form-check-input"
+ id="user-generate-totp"
+ type="checkbox"
+ checked={this.state.saveUserSettingsForm.generate_totp_2fa}
+ onChange={linkEvent(this, this.handleGenerateTotp)}
+ />
+ <label className="form-check-label" htmlFor="user-generate-totp">
+ {i18n.t("set_up_two_factor")}
+ </label>
+ </div>
+ </div>
+ )}
+
+ {totpUrl && (
+ <>
+ <div>
+ <a className="btn btn-secondary mb-2" href={totpUrl}>
+ {i18n.t("two_factor_link")}
+ </a>
+ </div>
+ <div className="form-group">
+ <div className="form-check">
+ <input
+ className="form-check-input"
+ id="user-remove-totp"
+ type="checkbox"
+ checked={
+ this.state.saveUserSettingsForm.generate_totp_2fa == false
+ }
+ onChange={linkEvent(this, this.handleRemoveTotp)}
+ />
+ <label className="form-check-label" htmlFor="user-remove-totp">
+ {i18n.t("remove_two_factor")}
+ </label>
+ </div>
+ </div>
+ </>
+ )}
+ </>
+ );
+ }
+
setupBlockPersonChoices() {
if (isBrowser()) {
let selectId: any = document.getElementById("block-person-filter");
i.setState(i.state);
}
+ handleGenerateTotp(i: Settings, event: any) {
+ // Coerce false to undefined here, so it won't generate it.
+ let checked: boolean | undefined = event.target.checked || undefined;
+ if (checked) {
+ toast(i18n.t("two_factor_setup_instructions"));
+ }
+ i.state.saveUserSettingsForm.generate_totp_2fa = checked;
+ i.setState(i.state);
+ }
+
+ handleRemoveTotp(i: Settings, event: any) {
+ // Coerce true to undefined here, so it won't generate it.
+ let checked: boolean | undefined = !event.target.checked && undefined;
+ i.state.saveUserSettingsForm.generate_totp_2fa = checked;
+ i.setState(i.state);
+ }
+
handleSendNotificationsToEmailChange(i: Settings, event: any) {
i.state.saveUserSettingsForm.send_notifications_to_email =
event.target.checked;
backgroundColor: backgroundColor,
gravity: "bottom",
position: "left",
+ duration: 5000,
}).showToast();
}
}
resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912"
integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==
-lemmy-js-client@0.17.2-rc.1:
- version "0.17.2-rc.1"
- resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.2-rc.1.tgz#fe8d1508311bbf245acc98c2c3e47e2165a95b14"
- integrity sha512-YrOXuCofgkqp28krmPTQZAfUWL5zEDA0sRJ0abKcgf/I8YYkYkUkPS9TOORN5Lv3bc8RAAz4+2/zLHqYL/Tnow==
+lemmy-js-client@0.17.2-rc.3:
+ version "0.17.2-rc.3"
+ resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.2-rc.3.tgz#dc2a33e9228aef260b03a6e1f55698a2f975f979"
+ integrity sha512-FlWEPMrW2Q/FbtihLOHq2YtcRuoX7700LweCnsm6R6dD6SzsnWy9nKJhn24fcjcR2o6tw0oZKgP0ccq9jPDgfQ==
dependencies:
node-fetch "2.6.6"