Refactor code

This commit is contained in:
Pierre HUBERT 2021-05-14 19:09:01 +02:00
parent 69c68f43cb
commit 6c4427d06b
7 changed files with 322 additions and 291 deletions

View File

@ -17,6 +17,7 @@ export interface AdminAccount {
name: string; name: string;
email: string; email: string;
time_create: number; time_create: number;
roles: Array<"manage_admins" | "manage_users" | "access_full_admin_logs">;
} }
export interface NewAdminGeneralSettings { export interface NewAdminGeneralSettings {
@ -32,7 +33,7 @@ export interface AdminResetToken {
export const SESSION_STORAGE_TOKEN = "auth_token"; export const SESSION_STORAGE_TOKEN = "auth_token";
let currentAccount: AdminAccount | null = null; let currentAccount: AdminAccount;
export class AccountHelper { export class AccountHelper {
/** /**

View File

@ -12,7 +12,7 @@ export interface AdminRole {
description: string; description: string;
} }
let RolesList: AdminRole[] = []; export let RolesList: AdminRole[] = [];
export class AdminRolesHelper { export class AdminRolesHelper {
/** /**

View File

@ -0,0 +1,93 @@
/**
* Account general settings section
*
* @author Pierre Hubert
*/
import { TextField, Button } from "@material-ui/core";
import React from "react";
import { AdminAccount, AccountHelper } from "../../helpers/AccountHelper";
import { snackbar, matAlert } from "../widgets/DialogsProvider";
import { SettingsSection } from "./SettingsSection";
export class GeneralSettings extends React.Component<
{ admin: AdminAccount },
{ newName: string; newEmail: string }
> {
constructor(p: any) {
super(p);
this.state = {
newName: this.props.admin.name,
newEmail: this.props.admin.email,
};
this.changedName = this.changedName.bind(this);
this.changedEmail = this.changedEmail.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
changedName(e: React.ChangeEvent<HTMLInputElement>) {
this.setState({ newName: e.target.value });
}
changedEmail(e: React.ChangeEvent<HTMLInputElement>) {
this.setState({ newEmail: e.target.value });
}
get isValid(): boolean {
return this.state.newEmail.length > 2 && this.state.newName.length > 2;
}
async handleSubmit() {
try {
if (!this.isValid) return;
await AccountHelper.UpdateGeneralSettings({
id: this.props.admin.id,
name: this.state.newName,
email: this.state.newEmail,
});
snackbar("Successfully updated admin settings!");
} catch (e) {
console.error(e);
matAlert("Failed to update admin settings!");
}
}
render() {
return (
<SettingsSection title="General settings">
<div style={{ margin: "10px" }}>
<TextField
required
label="Name"
value={this.state.newName}
onChange={this.changedName}
style={{ width: "100%", paddingBottom: "20px" }}
/>
<TextField
required
label="Email"
value={this.state.newEmail}
onChange={this.changedEmail}
type="mail"
style={{ width: "100%", paddingBottom: "20px" }}
/>
<div style={{ textAlign: "right" }}>
<Button
style={{ alignSelf: "end", marginRight: "10px" }}
disabled={!this.isValid}
onClick={this.handleSubmit}
>
Update
</Button>
</div>
</div>
</SettingsSection>
);
}
}

View File

@ -0,0 +1,186 @@
import {
Table,
TableHead,
TableRow,
TableCell,
TableBody,
IconButton,
Divider,
Button,
} from "@material-ui/core";
import { Delete } from "@material-ui/icons";
import React from "react";
import { AdminAccount, AccountHelper } from "../../helpers/AccountHelper";
import { AdminAccountKey, AdminKeyHelper } from "../../helpers/AdminKeyHelper";
import { CopyToClipboard } from "../../utils/ClipboardUtils";
import { AsyncWidget } from "../widgets/AsyncWidget";
import {
matConfirm,
snackbar,
matAlert,
input,
} from "../widgets/DialogsProvider";
import { TimestampWidget } from "../widgets/TimestampWidget";
import { SettingsSection } from "./SettingsSection";
export class KeySettingsSection extends React.Component<
{ admin: AdminAccount },
{ keys: AdminAccountKey[]; counter: number }
> {
constructor(props: any) {
super(props);
this.state = {
keys: [],
counter: 1,
};
this.load = this.load.bind(this);
this.build = this.build.bind(this);
this.generateResetToken = this.generateResetToken.bind(this);
this.registerNewKey = this.registerNewKey.bind(this);
this.deleteKey = this.deleteKey.bind(this);
}
async load() {
const keys = await AdminKeyHelper.GetAdminKeys(this.props.admin.id);
this.setState({ keys: keys });
}
async generateResetToken() {
try {
if (
!(await matConfirm(
"Do you really want to generate a reset token for this account?"
))
)
return;
const token = await AccountHelper.GenerateResetToken(
this.props.admin.id
);
CopyToClipboard(token.token);
snackbar("Reset token was successfully copied to the clipboard!");
} catch (e) {
console.error(e);
matAlert("Failed to generate a token!");
}
}
async registerNewKey() {
try {
const challenge =
await AdminKeyHelper.GetKeyRegistrationChallenge();
const credential = await navigator.credentials.create(challenge);
if (credential == null) throw new Error("Operation aborted!");
const name = await input({
label: "Key name",
maxLength: 40,
minLength: 2,
});
await AdminKeyHelper.RegisterKey(name, credential);
snackbar("Successfully enrolled a new key!");
this.setState({ counter: this.state.counter + 1 });
} catch (e) {
console.error(e);
matAlert("Failed to register a new key!");
}
}
async deleteKey(key: AdminAccountKey) {
try {
if (
!(await matConfirm(
"Do you really want to delete the key '" + key.name + "' ?"
))
)
return;
await AdminKeyHelper.DeleteAuthKey(this.props.admin.id, key.id);
snackbar("The key was successfully deleted!");
this.setState({ counter: this.state.counter + 1 });
} catch (e) {
console.error(e);
matAlert("Failed to delete key!");
}
}
render() {
return (
<AsyncWidget
errorMessage="Failed to load admin keys!"
load={this.load}
onBuild={this.build}
key={this.props.admin.id + "-" + this.state.counter}
></AsyncWidget>
);
}
build() {
return (
<SettingsSection title="Security keys">
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Key name</TableCell>
<TableCell align="right">Date added</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{this.state.keys.map((key) => (
<TableRow key={key.id}>
<TableCell component="th" scope="row">
{key.name}
</TableCell>
<TableCell align="right">
<TimestampWidget time={key.time_add} />
</TableCell>
<TableCell align="right">
<IconButton
aria-label="delete"
size="small"
onClick={() => this.deleteKey(key)}
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Divider />
{/* Action buttons */}
<div
style={{
textAlign: "right",
margin: "5px 10px",
}}
>
<Button onClick={this.generateResetToken}>
New reset token
</Button>
<Button
disabled={
this.props.admin.id !==
AccountHelper.currentAccount.id
}
onClick={this.registerNewKey}
>
Register a new key
</Button>
</div>
</SettingsSection>
);
}
}

View File

@ -0,0 +1,26 @@
import { Grid, Paper, Typography, Divider } from "@material-ui/core";
import React from "react";
export function SettingsSection(p: {
title: string;
children?: React.ReactNode;
}) {
return (
<Grid item sm={6}>
<Paper>
<Typography variant="h6" style={{ padding: "10px 15px " }}>
{p.title}
</Typography>
<Divider />
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
{p.children}
</div>
</Paper>
</Grid>
);
}

View File

@ -4,35 +4,14 @@
* @author Pierre Hubert * @author Pierre Hubert
*/ */
import { import { Grid } from "@material-ui/core";
Button,
Divider,
Grid,
IconButton,
Paper,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TextField,
Typography,
} from "@material-ui/core";
import { Delete } from "@material-ui/icons";
import React from "react"; import React from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { AccountHelper, AdminAccount } from "../../helpers/AccountHelper"; import { AccountHelper, AdminAccount } from "../../helpers/AccountHelper";
import { AdminAccountKey, AdminKeyHelper } from "../../helpers/AdminKeyHelper"; import { GeneralSettings } from "../accountSettings/GeneralSettings";
import { CopyToClipboard } from "../../utils/ClipboardUtils"; import { KeySettingsSection } from "../accountSettings/KeySettingsSection";
import { AsyncWidget } from "../widgets/AsyncWidget"; import { AsyncWidget } from "../widgets/AsyncWidget";
import {
input,
matAlert,
matConfirm,
snackbar,
} from "../widgets/DialogsProvider";
import { PageTitle } from "../widgets/PageTitle"; import { PageTitle } from "../widgets/PageTitle";
import { TimestampWidget } from "../widgets/TimestampWidget";
export function AccountSettingsRoute() { export function AccountSettingsRoute() {
let params: any = useParams(); let params: any = useParams();
@ -93,268 +72,3 @@ class AccountSettingsRouteInner extends React.Component<
); );
} }
} }
class GeneralSettings extends React.Component<
{ admin: AdminAccount },
{ newName: string; newEmail: string }
> {
constructor(p: any) {
super(p);
this.state = {
newName: this.props.admin.name,
newEmail: this.props.admin.email,
};
this.changedName = this.changedName.bind(this);
this.changedEmail = this.changedEmail.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
changedName(e: React.ChangeEvent<HTMLInputElement>) {
this.setState({ newName: e.target.value });
}
changedEmail(e: React.ChangeEvent<HTMLInputElement>) {
this.setState({ newEmail: e.target.value });
}
get isValid(): boolean {
return this.state.newEmail.length > 2 && this.state.newName.length > 2;
}
async handleSubmit() {
try {
if (!this.isValid) return;
await AccountHelper.UpdateGeneralSettings({
id: this.props.admin.id,
name: this.state.newName,
email: this.state.newEmail,
});
snackbar("Successfully updated admin settings!");
} catch (e) {
console.error(e);
matAlert("Failed to update admin settings!");
}
}
render() {
return (
<SettingsSection title="General settings">
<div style={{ margin: "10px" }}>
<TextField
required
label="Name"
value={this.state.newName}
onChange={this.changedName}
style={{ width: "100%", paddingBottom: "20px" }}
/>
<TextField
required
label="Email"
value={this.state.newEmail}
onChange={this.changedEmail}
type="mail"
style={{ width: "100%", paddingBottom: "20px" }}
/>
<div style={{ textAlign: "right" }}>
<Button
style={{ alignSelf: "end", marginRight: "10px" }}
disabled={!this.isValid}
onClick={this.handleSubmit}
>
Update
</Button>
</div>
</div>
</SettingsSection>
);
}
}
export class KeySettingsSection extends React.Component<
{ admin: AdminAccount },
{ keys: AdminAccountKey[]; counter: number }
> {
constructor(props: any) {
super(props);
this.state = {
keys: [],
counter: 1,
};
this.load = this.load.bind(this);
this.build = this.build.bind(this);
this.generateResetToken = this.generateResetToken.bind(this);
this.registerNewKey = this.registerNewKey.bind(this);
this.deleteKey = this.deleteKey.bind(this);
}
async load() {
const keys = await AdminKeyHelper.GetAdminKeys(this.props.admin.id);
this.setState({ keys: keys });
}
async generateResetToken() {
try {
if (
!(await matConfirm(
"Do you really want to generate a reset token for this account?"
))
)
return;
const token = await AccountHelper.GenerateResetToken(
this.props.admin.id
);
CopyToClipboard(token.token);
snackbar("Reset token was successfully copied to the clipboard!");
} catch (e) {
console.error(e);
matAlert("Failed to generate a token!");
}
}
async registerNewKey() {
try {
const challenge =
await AdminKeyHelper.GetKeyRegistrationChallenge();
const credential = await navigator.credentials.create(challenge);
if (credential == null) throw new Error("Operation aborted!");
const name = await input({
label: "Key name",
maxLength: 40,
minLength: 2,
});
await AdminKeyHelper.RegisterKey(name, credential);
snackbar("Successfully enrolled a new key!");
this.setState({ counter: this.state.counter + 1 });
} catch (e) {
console.error(e);
matAlert("Failed to register a new key!");
}
}
async deleteKey(key: AdminAccountKey) {
try {
if (
!(await matConfirm(
"Do you really want to delete the key '" + key.name + "' ?"
))
)
return;
await AdminKeyHelper.DeleteAuthKey(this.props.admin.id, key.id);
snackbar("The key was successfully deleted!");
this.setState({ counter: this.state.counter + 1 });
} catch (e) {
console.error(e);
matAlert("Failed to delete key!");
}
}
render() {
return (
<AsyncWidget
errorMessage="Failed to load admin keys!"
load={this.load}
onBuild={this.build}
key={this.props.admin.id + "-" + this.state.counter}
></AsyncWidget>
);
}
build() {
return (
<SettingsSection title="Security keys">
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Key name</TableCell>
<TableCell align="right">Date added</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{this.state.keys.map((key) => (
<TableRow key={key.id}>
<TableCell component="th" scope="row">
{key.name}
</TableCell>
<TableCell align="right">
<TimestampWidget time={key.time_add} />
</TableCell>
<TableCell align="right">
<IconButton
aria-label="delete"
size="small"
onClick={() => this.deleteKey(key)}
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Divider />
{/* Action buttons */}
<div
style={{
textAlign: "right",
margin: "5px 10px",
}}
>
<Button onClick={this.generateResetToken}>
New reset token
</Button>
<Button
disabled={
this.props.admin.id !==
AccountHelper.currentAccount.id
}
onClick={this.registerNewKey}
>
Register a new key
</Button>
</div>
</SettingsSection>
);
}
}
function SettingsSection(p: { title: string; children?: React.ReactNode }) {
return (
<Grid item sm={6}>
<Paper>
<Typography variant="h6" style={{ padding: "10px 15px " }}>
{p.title}
</Typography>
<Divider />
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
{p.children}
</div>
</Paper>
</Grid>
);
}

11
src/utils/AccountUtils.ts Normal file
View File

@ -0,0 +1,11 @@
/**
* Account utilities
*
* @author Pierre Hubert
*/
import { AccountHelper } from "../helpers/AccountHelper";
export function canManageAdmins(): boolean {
return AccountHelper.currentAccount.roles.includes("manage_admins");
}