OpenAuth_server/views/src/login/login.tsx

435 lines
12 KiB
TypeScript

import { h, Component, render } from "preact";
import "inputs";
import "./u2f-api-polyfill";
import sha from "sha512";
import { setCookie, getCookie } from "cookie";
let appname = "test";
function Loader() {
return (
<div class="loader_box" id="loader">
<div class="loader"></div>
</div>
);
}
class Username extends Component<
{ username: string; onNext: (username: string, salt: string) => void },
{ error: string; loading: boolean }
> {
username_input: HTMLInputElement;
constructor() {
super();
this.state = { error: undefined, loading: false };
}
async onClick() {
this.setState({ loading: true });
try {
let res = await fetch(
"/api/user/login?type=username&username=" +
this.username_input.value,
{
method: "POST",
}
)
.then((e) => {
if (e.status !== 200) throw new Error(e.statusText);
return e.json();
})
.then((data) => {
if (data.error) {
return Promise.reject(new Error(data.error));
}
return data;
});
let salt = res.salt;
this.props.onNext(this.username_input.value, salt);
} catch (err) {
this.setState({
error: err.message,
});
}
this.setState({ loading: false });
}
render() {
if (this.state.loading) return <Loader />;
return (
<div>
<div class="floating group">
<input
onKeyDown={(e) => {
let k = e.keyCode | e.which;
if (k === 13) this.onClick();
this.setState({ error: undefined });
}}
type="text"
value={
this.username_input
? this.username_input.value
: this.props.username
}
autofocus
ref={(elm) => (elm ? (this.username_input = elm) : undefined)}
/>
<span class="highlight"></span>
<span class="bar"></span>
<label>Username or Email</label>
{this.state.error ? (
<div class="error"> {this.state.error}</div>
) : undefined}
</div>
<button
type="button"
class="mdc-button mdc-button--raised spanned-btn"
onClick={() => this.onClick()}
>
Next
</button>
</div>
);
}
}
enum TFATypes {
OTC,
BACKUP_CODE,
YUBI_KEY,
APP_ALLOW,
}
interface TwoFactors {
id: string;
name: string;
type: TFATypes;
}
class Password extends Component<
{
username: string;
salt: string;
onNext: (login: Token, special: Token, tfa: TwoFactors[]) => void;
},
{ error: string; loading: boolean }
> {
password_input: HTMLInputElement;
constructor() {
super();
this.state = { error: undefined, loading: false };
}
async onClick() {
this.setState({
loading: true,
});
try {
let pw = sha(this.props.salt + this.password_input.value);
let { login, special, tfa } = await fetch(
"/api/user/login?type=password",
{
method: "POST",
body: JSON.stringify({
username: this.props.username,
password: pw,
}),
headers: {
"content-type": "application/json",
},
}
)
.then((e) => {
if (e.status !== 200) throw new Error(e.statusText);
return e.json();
})
.then((data) => {
if (data.error) {
return Promise.reject(new Error(data.error));
}
return data;
});
this.props.onNext(login, special, tfa);
} catch (err) {
this.setState({ error: err.messagae });
}
this.setState({ loading: false });
}
render() {
if (this.state.loading) return <Loader />;
return (
<div>
<div class="floating group">
<input
onKeyDown={(e) => {
let k = e.keyCode | e.which;
if (k === 13) this.onClick();
this.setState({ error: undefined });
}}
type="password"
ref={(elm: HTMLInputElement) => {
if (elm) {
this.password_input = elm;
setTimeout(() => elm.focus(), 200);
// elm.focus();
}
}}
/>
<span class="highlight"></span>
<span class="bar"></span>
<label>Password</label>
{this.state.error ? (
<div class="error"> {this.state.error}</div>
) : undefined}
</div>
<button
type="button"
class="mdc-button mdc-button--raised spanned-btn"
onClick={() => this.onClick()}
>
Login
</button>
</div>
);
}
}
class TwoFactor extends Component<
{ twofactors: TwoFactors[]; next: (id: string, type: TFATypes) => void },
{}
> {
render() {
let tfs = this.props.twofactors.map((fac) => {
let name: string;
switch (fac.type) {
case TFATypes.OTC:
name = "Authenticator";
break;
case TFATypes.BACKUP_CODE:
name = "Backup code";
break;
case TFATypes.APP_ALLOW:
name = "Use App: %s";
break;
case TFATypes.YUBI_KEY:
name = "Use Yubikey: %s";
break;
}
name = name.replace("%s", fac.name ? fac.name : "");
return (
<li
onClick={() => {
console.log("Click on Solution");
this.props.next(fac.id, fac.type);
}}
>
{name}
</li>
);
});
return (
<div>
<h1>Select one</h1>
<ul>{tfs}</ul>
</div>
);
}
}
// class TFA_YubiKey extends Component<{ id: string, login: Token, special: Token, next: (login: Token, special: Token) => void }, {}> {
// render() {
// }
// }
enum Page {
username,
password,
twofactor,
yubikey,
}
interface Token {
token: string;
expires: string;
}
async function apiRequest(
endpoint: string,
method: "GET" | "POST" | "DELETE" | "PUT" = "GET",
body: string = undefined
) {
return fetch(endpoint, {
method,
body,
credentials: "same-origin",
headers: {
"content-type": "application/json",
},
})
.then((e) => {
if (e.status !== 200) throw new Error(e.statusText);
return e.json();
})
.then((data) => {
if (data.error) {
return Promise.reject(new Error(data.error));
}
return data;
});
}
class App extends Component<
{},
{
page: Page;
username: string;
salt: string;
twofactor: TwoFactors[];
twofactor_id: string;
}
> {
login: Token;
special: Token;
constructor() {
super();
this.state = {
page: Page.username,
username: getCookie("username"),
salt: undefined,
twofactor: [],
twofactor_id: null,
};
}
setCookies() {
setCookie(
"login",
this.login.token,
new Date(this.login.expires).toUTCString()
);
setCookie(
"special",
this.special.token,
new Date(this.special.expires).toUTCString()
);
}
finish() {
this.setCookies();
let d = new Date();
d.setTime(d.getTime() + 30 * 24 * 60 * 60 * 1000); //Keep the username 30 days
setCookie("username", this.state.username, d.toUTCString());
let url = new URL(window.location.href);
let state = url.searchParams.get("state");
let red = "/";
if (state) {
let base64 = url.searchParams.get("base64");
if (base64) red = atob(state);
else red = state;
}
window.location.href = red;
}
render() {
let cont;
switch (this.state.page) {
case Page.username:
cont = (
<Username
username={this.state.username}
onNext={(username, salt) => {
this.setState({ username, salt, page: Page.password });
localStorage.setItem("username", username);
}}
/>
);
break;
case Page.password:
cont = (
<Password
username={this.state.username}
salt={this.state.salt}
onNext={(login, special, twofactor) => {
this.login = login;
this.special = special;
this.setCookies();
if (!twofactor) {
this.finish();
} else {
this.setState({ twofactor, page: Page.twofactor });
}
}}
/>
);
break;
case Page.twofactor:
cont = (
<TwoFactor
twofactors={this.state.twofactor}
next={async (id, type) => {
if (type === TFATypes.YUBI_KEY) {
let { request } = await apiRequest(
"/api/user/twofactor/yubikey",
"GET"
);
console.log(request);
(window as any).u2f.sign(
request.appId,
[request.challenge],
[request],
async (response) => {
let res = await apiRequest(
"/api/user/twofactor/yubikey",
"PUT",
JSON.stringify({ response })
);
if (res.success) {
this.login.expires = res.login_exp;
this.special.expires = res.special_exp;
this.finish();
}
}
);
}
}}
/>
);
break;
// case Page.yubikey:
// cont = <TFA_YubiKey id={this.state.twofactor_id} login={this.login} special={this.special} next={(login, special) => {
// this.login = login;
// this.special = special;
// this.finish()
// }} />
// break;
}
return (
<div>
<header>
<h1>Login</h1>
</header>
<form action="JavaScript:void(0)">{cont}</form>
<footer>
<p>Powered by {appname}</p>
</footer>
</div>
);
}
}
document.addEventListener(
"DOMContentLoaded",
function () {
render(<App />, document.body.querySelector("#content"));
},
false
);