mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-01 16:51:13 +00:00
Adding 2FA support. Fixes #938
This commit is contained in:
parent
deffaf1ee0
commit
af1e8868bd
6 changed files with 120 additions and 9 deletions
|
@ -1 +1 @@
|
|||
Subproject commit 819531ae64c6cba12cb406eb98333fd52988bf3e
|
||||
Subproject commit 1c42c579460871de7b4ea18e58dc25543b80d289
|
|
@ -45,7 +45,7 @@
|
|||
"inferno-server": "^8.0.5",
|
||||
"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.2",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-container": "^3.0.0",
|
||||
"markdown-it-footnote": "^3.0.3",
|
||||
|
|
|
@ -26,8 +26,10 @@ interface State {
|
|||
form: {
|
||||
username_or_email?: string;
|
||||
password?: string;
|
||||
totp_token?: string;
|
||||
};
|
||||
loginLoading: boolean;
|
||||
showTotp: boolean;
|
||||
siteRes: GetSiteResponse;
|
||||
}
|
||||
|
||||
|
@ -38,6 +40,7 @@ export class Login extends Component<any, State> {
|
|||
state: State = {
|
||||
form: {},
|
||||
loginLoading: false,
|
||||
showTotp: false,
|
||||
siteRes: this.isoData.site_res,
|
||||
};
|
||||
|
||||
|
@ -141,6 +144,28 @@ export class Login extends Component<any, State> {
|
|||
</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_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">
|
||||
|
@ -159,10 +184,12 @@ export class Login extends Component<any, State> {
|
|||
let lForm = i.state.form;
|
||||
let username_or_email = lForm.username_or_email;
|
||||
let password = lForm.password;
|
||||
let totp_token = lForm.totp_token;
|
||||
if (username_or_email && password) {
|
||||
let form: LoginI = {
|
||||
username_or_email,
|
||||
password,
|
||||
totp_token,
|
||||
};
|
||||
WebSocketService.Instance.send(wsClient.login(form));
|
||||
}
|
||||
|
@ -173,6 +200,11 @@ export class Login extends Component<any, State> {
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleLoginTotpChange(i: Login, event: any) {
|
||||
i.state.form.totp_token = event.target.value;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleLoginPasswordChange(i: Login, event: any) {
|
||||
i.state.form.password = event.target.value;
|
||||
i.setState(i.state);
|
||||
|
@ -191,9 +223,16 @@ export class Login extends Component<any, 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);
|
||||
|
|
|
@ -86,6 +86,7 @@ interface SettingsState {
|
|||
show_read_posts?: boolean;
|
||||
show_new_post_notifs?: boolean;
|
||||
discussion_languages?: number[];
|
||||
generate_totp?: boolean;
|
||||
};
|
||||
changePasswordForm: {
|
||||
new_password?: string;
|
||||
|
@ -789,6 +790,7 @@ export class Settings extends Component<any, SettingsState> {
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{this.totpSection()}
|
||||
<div className="form-group">
|
||||
<button type="submit" className="btn btn-block btn-secondary mr-4">
|
||||
{this.state.saveUserSettingsLoading ? (
|
||||
|
@ -853,6 +855,58 @@ export class Settings extends Component<any, SettingsState> {
|
|||
);
|
||||
}
|
||||
|
||||
totpSection() {
|
||||
let totpUrl =
|
||||
UserService.Instance.myUserInfo?.local_user_view.local_user.totp_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}
|
||||
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 == 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");
|
||||
|
@ -1017,6 +1071,23 @@ export class Settings extends Component<any, SettingsState> {
|
|||
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 = 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 = checked;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleSendNotificationsToEmailChange(i: Settings, event: any) {
|
||||
i.state.saveUserSettingsForm.send_notifications_to_email =
|
||||
event.target.checked;
|
||||
|
|
|
@ -500,6 +500,7 @@ export function toast(text: string, background = "success") {
|
|||
backgroundColor: backgroundColor,
|
||||
gravity: "bottom",
|
||||
position: "left",
|
||||
duration: 5000,
|
||||
}).showToast();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5324,10 +5324,10 @@ leac@^0.6.0:
|
|||
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.2:
|
||||
version "0.17.2-rc.2"
|
||||
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.17.2-rc.2.tgz#c2b045742c44992ec563c75a17cc1c7ebbb385eb"
|
||||
integrity sha512-Nsb0CnQ8JA8KlhDxb4Tri1QTT8l+4KMjbh17hBmMTjXzlSbo0xDyGBzF60Z2ffKu1ZPPll1ncO2tJeqCroin6w==
|
||||
dependencies:
|
||||
node-fetch "2.6.6"
|
||||
|
||||
|
|
Loading…
Reference in a new issue