diff --git a/src/helpers/AccountHelper.ts b/src/helpers/AccountHelper.ts index a1f83a2..794efef 100644 --- a/src/helpers/AccountHelper.ts +++ b/src/helpers/AccountHelper.ts @@ -17,6 +17,7 @@ export interface AdminAccount { name: string; email: string; time_create: number; + roles: Array<"manage_admins" | "manage_users" | "access_full_admin_logs">; } export interface NewAdminGeneralSettings { @@ -32,7 +33,7 @@ export interface AdminResetToken { export const SESSION_STORAGE_TOKEN = "auth_token"; -let currentAccount: AdminAccount | null = null; +let currentAccount: AdminAccount; export class AccountHelper { /** diff --git a/src/helpers/AdminRolesHelper.ts b/src/helpers/AdminRolesHelper.ts index af9810f..fa60d4e 100644 --- a/src/helpers/AdminRolesHelper.ts +++ b/src/helpers/AdminRolesHelper.ts @@ -12,7 +12,7 @@ export interface AdminRole { description: string; } -let RolesList: AdminRole[] = []; +export let RolesList: AdminRole[] = []; export class AdminRolesHelper { /** diff --git a/src/ui/accountSettings/GeneralSettings.tsx b/src/ui/accountSettings/GeneralSettings.tsx new file mode 100644 index 0000000..d66df0a --- /dev/null +++ b/src/ui/accountSettings/GeneralSettings.tsx @@ -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) { + this.setState({ newName: e.target.value }); + } + + changedEmail(e: React.ChangeEvent) { + 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 ( + +
+ + + + +
+ +
+
+
+ ); + } +} diff --git a/src/ui/accountSettings/KeySettingsSection.tsx b/src/ui/accountSettings/KeySettingsSection.tsx new file mode 100644 index 0000000..c4007ab --- /dev/null +++ b/src/ui/accountSettings/KeySettingsSection.tsx @@ -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 ( + + ); + } + + build() { + return ( + + + + + Key name + Date added + + + + + {this.state.keys.map((key) => ( + + + {key.name} + + + + + + this.deleteKey(key)} + > + + + + + ))} + +
+ + + + {/* Action buttons */} +
+ + +
+
+ ); + } +} diff --git a/src/ui/accountSettings/SettingsSection.tsx b/src/ui/accountSettings/SettingsSection.tsx new file mode 100644 index 0000000..9d03a69 --- /dev/null +++ b/src/ui/accountSettings/SettingsSection.tsx @@ -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 ( + + + + {p.title} + + +
+ {p.children} +
+
+
+ ); +} diff --git a/src/ui/routes/AccountSettingsRoute.tsx b/src/ui/routes/AccountSettingsRoute.tsx index 6ac2a81..cd7bf59 100644 --- a/src/ui/routes/AccountSettingsRoute.tsx +++ b/src/ui/routes/AccountSettingsRoute.tsx @@ -4,35 +4,14 @@ * @author Pierre Hubert */ -import { - Button, - Divider, - Grid, - IconButton, - Paper, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - TextField, - Typography, -} from "@material-ui/core"; -import { Delete } from "@material-ui/icons"; +import { Grid } from "@material-ui/core"; import React from "react"; import { useParams } from "react-router-dom"; import { AccountHelper, AdminAccount } from "../../helpers/AccountHelper"; -import { AdminAccountKey, AdminKeyHelper } from "../../helpers/AdminKeyHelper"; -import { CopyToClipboard } from "../../utils/ClipboardUtils"; +import { GeneralSettings } from "../accountSettings/GeneralSettings"; +import { KeySettingsSection } from "../accountSettings/KeySettingsSection"; import { AsyncWidget } from "../widgets/AsyncWidget"; -import { - input, - matAlert, - matConfirm, - snackbar, -} from "../widgets/DialogsProvider"; import { PageTitle } from "../widgets/PageTitle"; -import { TimestampWidget } from "../widgets/TimestampWidget"; export function AccountSettingsRoute() { 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) { - this.setState({ newName: e.target.value }); - } - - changedEmail(e: React.ChangeEvent) { - 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 ( - -
- - - - -
- -
-
-
- ); - } -} - -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 ( - - ); - } - - build() { - return ( - - - - - Key name - Date added - - - - - {this.state.keys.map((key) => ( - - - {key.name} - - - - - - this.deleteKey(key)} - > - - - - - ))} - -
- - - - {/* Action buttons */} -
- - -
-
- ); - } -} - -function SettingsSection(p: { title: string; children?: React.ReactNode }) { - return ( - - - - {p.title} - - -
- {p.children} -
-
-
- ); -} diff --git a/src/utils/AccountUtils.ts b/src/utils/AccountUtils.ts new file mode 100644 index 0000000..3a9abd3 --- /dev/null +++ b/src/utils/AccountUtils.ts @@ -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"); +}