Commit 66c7c3b2 authored by Paul Colin Hennig's avatar Paul Colin Hennig
Browse files

add 2fa interface

parent 25178ec6
This diff is collapsed.
import {
faCheckCircle,
faEnvelope,
faExclamationCircle,
} from "@fortawesome/free-solid-svg-icons";
import { faCheckCircle, faEnvelope, faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { Component } from "react";
import "./Login.css";
export default class Login extends Component {
state = {
email: "",
active: false,
response: false,
};
componentDidMount = () => {
if (this.props.location.hash !== "") {
this.props.history.push({ hash: "" });
}
};
handleInput = (e) => {
const a = e.target.value;
this.setState({ email: a });
state = {
email: "",
active: false,
response: false
};
componentDidMount = () => {
if (this.props.location.hash !== "") {
this.props.history.push({ hash: "" });
}
};
if (
a.match(
/^(([^<>()[\]\\.,;:\s@"]{1,64}(\.[^<>()[\]\\.,;:\s@"]+)*)|(".{1,62}"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]{1,63}\.)+[a-zA-Z]{2,63}))$/
)
) {
this.setState({ active: true });
} else {
this.setState({ active: false });
}
};
handleClick = async () => {
this.setState({ active: false });
handleInput = e => {
const a = e.target.value;
this.setState({ email: a });
fetch(`/akkount/login?email=${this.state.email}`, {
method: "POST",
})
.then(handleErrors)
.then((res) => {
console.log(res);
if (res === "OK") {
this.props.history.push({ hash: "#success" });
if (a.match(/^(([^<>()[\]\\.,;:\s@"]{1,64}(\.[^<>()[\]\\.,;:\s@"]+)*)|(".{1,62}"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]{1,63}\.)+[a-zA-Z]{2,63}))$/)) {
this.setState({ active: true });
} else {
this.props.history.push({ hash: "#error" });
this.setState({ active: false });
}
this.setState({ response: res, active: true });
});
};
render() {
const requestUi = () => {
return (
<div className="Request">
<label htmlFor="email">
<FontAwesomeIcon icon={faEnvelope}></FontAwesomeIcon> E-Mail
</label>
<input
onChange={this.handleInput}
autoFocus
spellCheck="false"
type="email"
name="email"
placeholder="dragon@example.com"
id="email"
value={this.state.email}
/>
<button
onClick={this.handleClick}
style={{ opacity: this.state.active ? 1 : 0.5 }}
disabled={!this.state.active}
>
Login
</button>
<p>
No classic registration or password required. Just type in your
e-mail address and we will send you a link to login. <br /> By
logging in you agree to our <a href="/privacy">Privacy Policy</a>
</p>
</div>
);
};
const responseUi = (res) => {
if (res === "OK") {
return (
<div className="Response">
<FontAwesomeIcon
className="check"
icon={faCheckCircle}
></FontAwesomeIcon>
<h1>Success!</h1>
<p>The mail was successfully sent to {this.state.email}</p>
<p>
Please click the link in the mail on the same device to login.
</p>
</div>
);
}
return (
<div className="Response">
<FontAwesomeIcon
className="exclamation"
icon={faExclamationCircle}
></FontAwesomeIcon>
<h1>Error!</h1>
<p>{this.state.response}</p>
<button onClick={() => this.props.history.goBack()}>Go back</button>
</div>
);
handleClick = async () => {
this.setState({ active: false });
fetch(`/akkount/login?email=${this.state.email}`, {
method: "POST"
})
.then(handleErrors)
.then(res => {
console.log(res);
if (res === "OK") {
this.props.history.push({ hash: "#success" });
} else {
this.props.history.push({ hash: "#error" });
}
this.setState({ response: res, active: true });
});
};
render() {
const requestUi = () => {
return (
<div className="Request">
<label htmlFor="email">
<FontAwesomeIcon icon={faEnvelope}></FontAwesomeIcon> E-Mail
</label>
<input onChange={this.handleInput} autoFocus spellCheck="false" type="email" name="email" placeholder="dragon@example.com" id="email" value={this.state.email} />
<button onClick={this.handleClick} style={{ opacity: this.state.active ? 1 : 0.5 }} disabled={!this.state.active}>
Login
</button>
<p>
No classic registration or password required. Just type in your e-mail address and we will send you a link to login. <br /> By logging in you agree to our{" "}
<a href="/privacy">Privacy Policy</a>.
</p>
</div>
);
};
const responseUi = res => {
if (res === "OK") {
return (
<div className="Response">
<FontAwesomeIcon className="check" icon={faCheckCircle}></FontAwesomeIcon>
<h1>Success!</h1>
<p>The mail was successfully sent to {this.state.email}</p>
<p>Please click the link in the mail on the same device to login.</p>
</div>
);
}
return (
<div className="Response">
<FontAwesomeIcon className="exclamation" icon={faExclamationCircle}></FontAwesomeIcon>
<h1>Error!</h1>
<p>{this.state.response}</p>
<button onClick={() => this.props.history.goBack()}>Go back</button>
</div>
);
};
return (
<div className="Login">
{(() => {
if (this.props.location.hash === "#success") {
return responseUi(this.state.response);
} else if (this.props.location.hash === "#error") {
return responseUi(this.state.response);
} else {
return requestUi();
}
})()}
</div>
);
}
return (
<div className="Login">
{(() => {
if (this.props.location.hash === "#success") {
return responseUi(this.state.response);
} else if (this.props.location.hash === "#error") {
return responseUi(this.state.response);
} else {
return requestUi();
}
})()}
</div>
);
}
}
const handleErrors = async (res) => {
if (res.ok) {
return await res.text().catch((e) => {});
}
return res.status + " - " + res.statusText;
export const handleErrors = async res => {
if (res.ok) {
return await res.text().catch(e => {});
}
return res.status + " - " + res.statusText;
};
.TwoFa {
text-align: center;
}
.TwoFa a {
text-decoration: underline;
}
.TwoFa > div {
max-width: 420px;
margin-left: auto;
margin-right: auto;
}
.TwoFa .round button {
width: 100px;
height: 100px;
border-radius: 50%;
background-color: var(--button-color);
font-weight: bold;
font-size: 16px;
}
.TwoFa button.skip {
background: var(--b2);
margin-top: 30px;
max-width: 200px;
width: 100%;
font-size: 24px;
padding: 10px;
font-family: "Open Sans", sans-serif;
}
.TwoFa > div {
padding: 10px;
box-sizing: border-box;
text-align: center;
}
.TwoFa > div > div {
width: 50%;
box-sizing: border-box;
float: left;
}
.TwoFa .U2F div {
margin-top: 10px;
}
.TwoFa .TOTP div {
margin-top: 15.5px;
}
.TwoFa .TOTPstage {
margin-top: 0vh;
}
.TwoFa input {
background: var(--b1);
color: var(--f1);
width: 100%;
border: none;
box-sizing: border-box;
border-bottom: 1px solid var(--m);
font-size: 24px;
padding: 10px;
text-align: center;
}
.TwoFa .TOTPstage button {
background: var(--button-color);
margin-top: 20px;
width: 100%;
font-size: 24px;
padding: 10px;
font-family: "Open Sans", sans-serif;
}
import React, { Component } from "react";
import "./TwoFa.css";
import { handleErrors } from "../login/Login";
export default class TwoFa extends Component {
state = { response: false, active: false, totpToken: "" };
constructor() {
super();
this.totp = false;
}
componentDidMount = () => {
if (this.props.location.hash !== "") {
this.props.history.push({ hash: "" });
}
};
totpClick = () => {
fetch("/akkount/2fa/totp/generate", { method: "POST" })
.then(handleErrors)
.then(res => {
if (res) {
this.totp = JSON.parse(res);
this.props.history.push({ hash: "#totp" });
} else {
this.props.history.push({ hash: "#error" });
}
});
};
handleTotpInput = () => {};
render() {
const totp = () => {
return (
<div className="TOTPstage">
<h1>Time-based One-Time Password</h1>
<p>
Scan this QR-Code with your TOTP app to add the shared secret to your device. <br /> If you dont have a TOTP app we recommend using{" "}
<a href="https://play.google.com/store/apps/details?id=org.shadowice.flocke.andotp">andOTP</a> for Android devices.
</p>
<div style={{ width: "100%" }}>
<img width="140" draggable="false" src={this.totp.qrCode} alt="Qr-Code" />
</div>
<div style={{ width: "100%" }}>
<p>If you can't scan the QR-Code you may copy the secret below and add it to your app this way. (SHA-1, 6 Chars)</p>
<h2 className="s">{this.totp.secret}</h2>
</div>
<div style={{ width: "100%", float: "top", height: "1em" }}></div>
<p>After adding the secret submit a generated OTP from the app to our server.</p>
<input value={this.state.totpToken} onChange={this.handleTotpInput} placeholder="Generated Token" type="text" />
<button style={{ opacity: this.state.active ? 1 : 0.5 }} disabled={!this.state.active}>
Submit
</button>
</div>
);
};
const choose = () => {
return (
<div>
<h1>Two Factor Authentication</h1>
<p>For better security we recommend you to use a second factor for authentication with our server.</p>
<div className="U2F round">
<h2>U2F</h2>
<button onClick={() => this.props.history.push({ hash: "#u2f" })}>{u2fGraphic(100)}</button>
<div>This method uses a hardware key. It's very convenient and extremely secure.</div>
</div>
<div className="TOTP round">
<h2>TOTP</h2>
<button onClick={this.totpClick}>*** ***</button>
<div>This method uses an app to generate time based one time passwords.</div>
</div>
<button className="skip">Skip</button>
</div>
);
};
return (
<div className="TwoFa">
{(() => {
if (this.props.location.hash === "#u2f") {
} else if (this.props.location.hash === "#totp") {
return totp();
} else {
return choose();
}
})()}
</div>
);
}
}
const u2fGraphic = size => {
return (
<svg style={{ fill: "var(--m)", transform: "translate(-7px, 1px) scale(0.7) rotateZ(45deg) " }} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width={size} height={size}>
<path
style={{ stroke: "var(--m)", strokeWidth: 10, strokeLinejoin: "round" }}
id="body"
d="M161 10L339.21 10L339.21 390.65L294.66 390.65L294.66 485.82L205.55 485.82L205.55 390.65L161 390.65L161 10Z"
/>
<g id="contacts" style={{ fill: "var(--f1)" }}>
<path d="M211.37 403.27L226.6 403.27L226.6 481.19L211.37 481.19L211.37 403.27Z" />
<path d="M232.31 403.27L247.54 403.27L247.54 468.82L232.31 468.82L232.31 403.27Z" />
<path d="M252.68 403.27L267.9 403.27L267.9 468.82L252.68 468.82L252.68 403.27Z" />
<path d="M273.61 403.27L288.84 403.27L288.84 481.19L273.61 481.19L273.61 403.27Z" />
</g>
<path
style={{ fill: "var(--button-color)" }}
id="hole"
d="M249.55 60.08C240.15 60.08 232.55 52.34 232.55 42.77C232.55 33.2 240.15 25.46 249.55 25.46C258.95 25.46 266.55 33.2 266.55 42.77C266.55 52.34 258.95 60.08 249.55 60.08Z"
/>
<path
style={{ fill: "var(--f1)" }}
id="center_circle"
d="M250.11 247.91C223.79 247.91 202.52 226.64 202.52 200.33C202.52 174.01 223.79 152.75 250.11 152.75C276.42 152.75 297.69 174.01 297.69 200.33C297.69 226.64 276.42 247.91 250.11 247.91Z"
/>
<path
style={{ fill: "var(--f1)", stroke: "var(--m)", strokeWidth: 7 }}
id="lock_top"
d="M250.11 204.23C243.79 204.23 238.69 199.13 238.69 192.81C238.69 186.5 243.79 181.39 250.11 181.39C256.42 181.39 261.53 186.5 261.53 192.81C261.53 199.13 256.42 204.23 250.11 204.23Z"
/>
<path id="lock_body" d="M231.07 192.71L268.28 192.71L268.28 225.07L231.07 225.07L231.07 192.71Z" />
</svg>
);
};
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment