Add token creation dialog
This commit is contained in:
158
matrixgw_frontend/package-lock.json
generated
158
matrixgw_frontend/package-lock.json
generated
@@ -15,6 +15,10 @@
|
|||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mui/icons-material": "^7.3.5",
|
"@mui/icons-material": "^7.3.5",
|
||||||
"@mui/material": "^7.3.5",
|
"@mui/material": "^7.3.5",
|
||||||
|
"@mui/x-date-pickers": "^8.17.0",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
|
"is-cidr": "^6.0.1",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router": "^7.9.5"
|
"react-router": "^7.9.5"
|
||||||
@@ -1039,6 +1043,94 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mui/x-date-pickers": {
|
||||||
|
"version": "8.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.17.0.tgz",
|
||||||
|
"integrity": "sha512-mrrkTJ1+r6MsPnKH/N5lCNJHkP0dZc2Fvd8fp5tyxa0jRyzwbxJKsadXooccoJWp65Z2vUjUuctXYUmubYP/Sg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.4",
|
||||||
|
"@mui/utils": "^7.3.3",
|
||||||
|
"@mui/x-internals": "8.17.0",
|
||||||
|
"@types/react-transition-group": "^4.4.12",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-transition-group": "^4.4.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/react": "^11.9.0",
|
||||||
|
"@emotion/styled": "^11.8.1",
|
||||||
|
"@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
|
||||||
|
"@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
|
||||||
|
"date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
|
||||||
|
"date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
|
||||||
|
"dayjs": "^1.10.7",
|
||||||
|
"luxon": "^3.0.2",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"moment-hijri": "^2.1.2 || ^3.0.0",
|
||||||
|
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@emotion/styled": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"date-fns": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"date-fns-jalali": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"dayjs": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"luxon": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"moment": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"moment-hijri": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"moment-jalaali": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mui/x-internals": {
|
||||||
|
"version": "8.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.17.0.tgz",
|
||||||
|
"integrity": "sha512-KvmR0PPX1j2i44y0DXwzs45jIPMu/YZYXYy7xvzo+ZNdYebbW5LbVeG4zdEUnKHyOG02oHdI7MM9AxcZE16TBw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.4",
|
||||||
|
"@mui/utils": "^7.3.3",
|
||||||
|
"reselect": "^5.1.1",
|
||||||
|
"use-sync-external-store": "^1.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
|
||||||
@@ -1987,6 +2079,18 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cidr-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-2Apfc6qH9uwF3QHmlYBA8ExB9VHq+1/Doj9sEMY55TVBcpQ3y/+gmMpcNIBBtfb5k54Vphmta+1IxjMqPlWWAA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-regex": "5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
@@ -2085,6 +2189,12 @@
|
|||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
|
||||||
|
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -2615,12 +2725,36 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-regex": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-arrayish": {
|
"node_modules/is-arrayish": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||||
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
|
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/is-cidr": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-cidr/-/is-cidr-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-JIJlvXodfsoWFAvvjB7Elqu8qQcys2SZjkIJCLdk4XherUqZ6+zH7WIpXkp4B3ZxMH0Fz7zIsZwyvs6JfM0csw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"cidr-regex": "5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-core-module": {
|
"node_modules/is-core-module": {
|
||||||
"version": "2.16.1",
|
"version": "2.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
@@ -3368,6 +3502,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode.react": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -3464,6 +3607,12 @@
|
|||||||
"react-dom": ">=16.6.0"
|
"react-dom": ">=16.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
@@ -3858,6 +4007,15 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"name": "rolldown-vite",
|
"name": "rolldown-vite",
|
||||||
"version": "7.1.14",
|
"version": "7.1.14",
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mui/icons-material": "^7.3.5",
|
"@mui/icons-material": "^7.3.5",
|
||||||
"@mui/material": "^7.3.5",
|
"@mui/material": "^7.3.5",
|
||||||
|
"@mui/x-date-pickers": "^8.17.0",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
|
"is-cidr": "^6.0.1",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router": "^7.9.5"
|
"react-router": "^7.9.5"
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import {
|
|||||||
} from "react-router";
|
} from "react-router";
|
||||||
import { AuthApi } from "./api/AuthApi";
|
import { AuthApi } from "./api/AuthApi";
|
||||||
import { ServerApi } from "./api/ServerApi";
|
import { ServerApi } from "./api/ServerApi";
|
||||||
|
import { APITokensRoute } from "./routes/APITokensRoute";
|
||||||
import { LoginRoute } from "./routes/auth/LoginRoute";
|
import { LoginRoute } from "./routes/auth/LoginRoute";
|
||||||
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
||||||
import { HomeRoute } from "./routes/HomeRoute";
|
import { HomeRoute } from "./routes/HomeRoute";
|
||||||
|
import { MatrixAuthCallback } from "./routes/MatrixAuthCallback";
|
||||||
import { MatrixLinkRoute } from "./routes/MatrixLinkRoute";
|
import { MatrixLinkRoute } from "./routes/MatrixLinkRoute";
|
||||||
import { NotFoundRoute } from "./routes/NotFoundRoute";
|
import { NotFoundRoute } from "./routes/NotFoundRoute";
|
||||||
import { BaseLoginPage } from "./widgets/auth/BaseLoginPage";
|
import { BaseLoginPage } from "./widgets/auth/BaseLoginPage";
|
||||||
import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage";
|
import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage";
|
||||||
import { MatrixAuthCallback } from "./routes/MatrixAuthCallback";
|
|
||||||
|
|
||||||
interface AuthContext {
|
interface AuthContext {
|
||||||
signedIn: boolean;
|
signedIn: boolean;
|
||||||
@@ -41,6 +42,7 @@ export function App(): React.ReactElement {
|
|||||||
<Route path="" element={<HomeRoute />} />
|
<Route path="" element={<HomeRoute />} />
|
||||||
<Route path="matrix_link" element={<MatrixLinkRoute />} />
|
<Route path="matrix_link" element={<MatrixLinkRoute />} />
|
||||||
<Route path="matrix_auth_cb" element={<MatrixAuthCallback />} />
|
<Route path="matrix_auth_cb" element={<MatrixAuthCallback />} />
|
||||||
|
<Route path="tokens" element={<APITokensRoute />} />
|
||||||
<Route path="*" element={<NotFoundRoute />} />
|
<Route path="*" element={<NotFoundRoute />} />
|
||||||
</Route>
|
</Route>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
46
matrixgw_frontend/src/api/TokensApi.ts
Normal file
46
matrixgw_frontend/src/api/TokensApi.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { APIClient } from "./ApiClient";
|
||||||
|
|
||||||
|
export interface BaseToken {
|
||||||
|
name: string;
|
||||||
|
networks?: string[];
|
||||||
|
max_inactivity: number;
|
||||||
|
expiration?: number;
|
||||||
|
read_only: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Token extends BaseToken {
|
||||||
|
id: number;
|
||||||
|
created: number;
|
||||||
|
last_used: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenWithSecret extends Token {
|
||||||
|
secret: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TokensApi {
|
||||||
|
/**
|
||||||
|
* Get the list of tokens of the current user
|
||||||
|
*/
|
||||||
|
static async GetList(): Promise<Token[]> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: "/tokens",
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new token
|
||||||
|
*/
|
||||||
|
static async Create(t: BaseToken): Promise<TokenWithSecret> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: "/token",
|
||||||
|
method: "POST",
|
||||||
|
jsonData: t,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
}
|
||||||
159
matrixgw_frontend/src/dialogs/CreateTokenDialog.tsx
Normal file
159
matrixgw_frontend/src/dialogs/CreateTokenDialog.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import { ServerApi } from "../api/ServerApi";
|
||||||
|
import {
|
||||||
|
TokensApi,
|
||||||
|
type BaseToken,
|
||||||
|
type TokenWithSecret,
|
||||||
|
} from "../api/TokensApi";
|
||||||
|
import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider";
|
||||||
|
import { useLoadingMessage } from "../hooks/contexts_provider/LoadingMessageProvider";
|
||||||
|
import { time } from "../utils/DateUtils";
|
||||||
|
import {
|
||||||
|
checkConstraint,
|
||||||
|
checkNumberConstraint,
|
||||||
|
isIPNetworkValid,
|
||||||
|
} from "../utils/FormUtils";
|
||||||
|
import { CheckboxInput } from "../widgets/forms/CheckboxInput";
|
||||||
|
import { DateInput } from "../widgets/forms/DateInput";
|
||||||
|
import { NetworksInput } from "../widgets/forms/NetworksInput";
|
||||||
|
import { TextInput } from "../widgets/forms/TextInput";
|
||||||
|
|
||||||
|
const SECS_IN_DAY = 3600 * 24;
|
||||||
|
|
||||||
|
export function CreateTokenDialog(p: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onCreated: (t: TokenWithSecret) => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const alert = useAlert();
|
||||||
|
const loadingMessage = useLoadingMessage();
|
||||||
|
|
||||||
|
const [newTokenUndef, setNewToken] = React.useState<BaseToken | undefined>();
|
||||||
|
const newToken: BaseToken = newTokenUndef ?? {
|
||||||
|
name: "",
|
||||||
|
max_inactivity: 3600 * 24 * 90,
|
||||||
|
read_only: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const valid =
|
||||||
|
checkConstraint(ServerApi.Config.constraints.token_name, newToken.name) ===
|
||||||
|
undefined &&
|
||||||
|
checkNumberConstraint(
|
||||||
|
ServerApi.Config.constraints.token_max_inactivity,
|
||||||
|
newToken.max_inactivity
|
||||||
|
) === undefined &&
|
||||||
|
(newToken.networks === undefined ||
|
||||||
|
newToken.networks.every((n) => isIPNetworkValid(n)));
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
loadingMessage.show("Creating access token...");
|
||||||
|
const token = await TokensApi.Create(newToken);
|
||||||
|
p.onCreated(token);
|
||||||
|
|
||||||
|
// Clear form
|
||||||
|
setNewToken(undefined);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to create token! ${e}`);
|
||||||
|
alert(`Failed to create API token! ${e}`);
|
||||||
|
} finally {
|
||||||
|
loadingMessage.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={p.open} onClose={p.onClose}>
|
||||||
|
<DialogTitle>Create new API token</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextInput
|
||||||
|
editable
|
||||||
|
required
|
||||||
|
label="Token name"
|
||||||
|
value={newToken.name}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setNewToken({
|
||||||
|
...newToken,
|
||||||
|
name: v ?? "",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size={ServerApi.Config.constraints.token_name}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NetworksInput
|
||||||
|
editable
|
||||||
|
label="Allowed networks (CIDR notation)"
|
||||||
|
value={newToken.networks}
|
||||||
|
onChange={(v) => {
|
||||||
|
setNewToken({
|
||||||
|
...newToken,
|
||||||
|
networks: v,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
editable
|
||||||
|
required
|
||||||
|
label="Max inactivity period (days)"
|
||||||
|
type="number"
|
||||||
|
value={(newToken.max_inactivity / SECS_IN_DAY).toString()}
|
||||||
|
onValueChange={(i) => {
|
||||||
|
setNewToken({
|
||||||
|
...newToken,
|
||||||
|
max_inactivity: Number(i) * SECS_IN_DAY,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size={{
|
||||||
|
min:
|
||||||
|
ServerApi.Config.constraints.token_max_inactivity.min /
|
||||||
|
SECS_IN_DAY,
|
||||||
|
max:
|
||||||
|
ServerApi.Config.constraints.token_max_inactivity.max /
|
||||||
|
SECS_IN_DAY,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
editable
|
||||||
|
label="Expiration date (optional)"
|
||||||
|
value={newToken.expiration}
|
||||||
|
onChange={(i) => {
|
||||||
|
setNewToken((t) => {
|
||||||
|
return {
|
||||||
|
...(t ?? newToken),
|
||||||
|
expiration: i ?? undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disablePast
|
||||||
|
checkValue={(s) => s > time()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CheckboxInput
|
||||||
|
editable
|
||||||
|
label="Read only"
|
||||||
|
checked={newToken.read_only}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setNewToken({
|
||||||
|
...newToken,
|
||||||
|
read_only: v,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={p.onClose}>Cancel</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={!valid} autoFocus>
|
||||||
|
Create token
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,21 +3,24 @@ import "@fontsource/roboto/400.css";
|
|||||||
import "@fontsource/roboto/500.css";
|
import "@fontsource/roboto/500.css";
|
||||||
import "@fontsource/roboto/700.css";
|
import "@fontsource/roboto/700.css";
|
||||||
|
|
||||||
|
import { CssBaseline } from "@mui/material";
|
||||||
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||||
|
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import "./index.css";
|
import { ServerApi } from "./api/ServerApi";
|
||||||
import { App } from "./App";
|
import { App } from "./App";
|
||||||
import { AlertDialogProvider } from "./hooks/contexts_provider/AlertDialogProvider";
|
import { AlertDialogProvider } from "./hooks/contexts_provider/AlertDialogProvider";
|
||||||
import { ConfirmDialogProvider } from "./hooks/contexts_provider/ConfirmDialogProvider";
|
import { ConfirmDialogProvider } from "./hooks/contexts_provider/ConfirmDialogProvider";
|
||||||
import { SnackbarProvider } from "./hooks/contexts_provider/SnackbarProvider";
|
|
||||||
import { LoadingMessageProvider } from "./hooks/contexts_provider/LoadingMessageProvider";
|
import { LoadingMessageProvider } from "./hooks/contexts_provider/LoadingMessageProvider";
|
||||||
import { AsyncWidget } from "./widgets/AsyncWidget";
|
import { SnackbarProvider } from "./hooks/contexts_provider/SnackbarProvider";
|
||||||
import { ServerApi } from "./api/ServerApi";
|
import "./index.css";
|
||||||
import { AppTheme } from "./theme/AppTheme";
|
import { AppTheme } from "./theme/AppTheme";
|
||||||
import { CssBaseline } from "@mui/material";
|
import { AsyncWidget } from "./widgets/AsyncWidget";
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="en">
|
||||||
<AppTheme>
|
<AppTheme>
|
||||||
<CssBaseline enableColorScheme />
|
<CssBaseline enableColorScheme />
|
||||||
<AlertDialogProvider>
|
<AlertDialogProvider>
|
||||||
@@ -37,5 +40,6 @@ createRoot(document.getElementById("root")!).render(
|
|||||||
</ConfirmDialogProvider>
|
</ConfirmDialogProvider>
|
||||||
</AlertDialogProvider>
|
</AlertDialogProvider>
|
||||||
</AppTheme>
|
</AppTheme>
|
||||||
|
</LocalizationProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
98
matrixgw_frontend/src/routes/APITokensRoute.tsx
Normal file
98
matrixgw_frontend/src/routes/APITokensRoute.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import { Alert, AlertTitle, IconButton, Tooltip } from "@mui/material";
|
||||||
|
import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer";
|
||||||
|
import React from "react";
|
||||||
|
import { CreateTokenDialog } from "../dialogs/CreateTokenDialog";
|
||||||
|
import { type TokenWithSecret } from "../api/TokensApi";
|
||||||
|
import { APIClient } from "../api/ApiClient";
|
||||||
|
import { QRCodeCanvas } from "qrcode.react";
|
||||||
|
import { CopyTextChip } from "../widgets/CopyTextChip";
|
||||||
|
|
||||||
|
export function APITokensRoute(): React.ReactElement {
|
||||||
|
const [openCreateTokenDialog, setOpenCreateTokenDialog] =
|
||||||
|
React.useState(false);
|
||||||
|
|
||||||
|
const [createdToken, setCreatedToken] =
|
||||||
|
React.useState<TokenWithSecret | null>(null);
|
||||||
|
|
||||||
|
const handleRefreshTokensList = () => {
|
||||||
|
//throw new Error("todo");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenCreateTokenDialog = () => setOpenCreateTokenDialog(true);
|
||||||
|
|
||||||
|
const handleCancelCreateToken = () => setOpenCreateTokenDialog(false);
|
||||||
|
|
||||||
|
const handleCreatedToken = (s: TokenWithSecret) => {
|
||||||
|
setCreatedToken(s);
|
||||||
|
setOpenCreateTokenDialog(false);
|
||||||
|
handleRefreshTokensList();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MatrixGWRouteContainer
|
||||||
|
label={"API tokens"}
|
||||||
|
actions={
|
||||||
|
<span>
|
||||||
|
<Tooltip title="Create new token">
|
||||||
|
<IconButton onClick={handleOpenCreateTokenDialog}>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title="Refresh tokens list">
|
||||||
|
<IconButton onClick={handleRefreshTokensList}>
|
||||||
|
<RefreshIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CreateTokenDialog
|
||||||
|
open={openCreateTokenDialog}
|
||||||
|
onCreated={handleCreatedToken}
|
||||||
|
onClose={handleCancelCreateToken}
|
||||||
|
/>
|
||||||
|
{createdToken && <CreatedToken token={createdToken!} />}
|
||||||
|
<p>TODO list</p>
|
||||||
|
</MatrixGWRouteContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreatedToken(p: { token: TokenWithSecret }): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Alert severity="success" style={{ margin: "10px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: "center", marginRight: "10px" }}>
|
||||||
|
<div style={{ padding: "15px", backgroundColor: "white" }}>
|
||||||
|
<QRCodeCanvas
|
||||||
|
value={`matrixgw://api=${encodeURIComponent(
|
||||||
|
APIClient.ActualBackendURL()
|
||||||
|
)}&id=${p.token.id}&secret=${p.token.secret}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<em>Mobile App Qr Code</em>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<AlertTitle>Token successfully created</AlertTitle>
|
||||||
|
The API token <i>{p.token.name}</i> was successfully created. Please
|
||||||
|
note the following information as they won't be available after.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
API URL: <CopyTextChip text={APIClient.ActualBackendURL()} />
|
||||||
|
<br />
|
||||||
|
Token ID: <CopyTextChip text={p.token.id.toString()} />
|
||||||
|
<br />
|
||||||
|
Token secret: <CopyTextChip text={p.token.secret} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
matrixgw_frontend/src/utils/DateUtils.ts
Normal file
8
matrixgw_frontend/src/utils/DateUtils.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Get UNIX time
|
||||||
|
*
|
||||||
|
* @returns Number of seconds since Epoch
|
||||||
|
*/
|
||||||
|
export function time(): number {
|
||||||
|
return Math.floor(new Date().getTime() / 1000);
|
||||||
|
}
|
||||||
52
matrixgw_frontend/src/utils/FormUtils.ts
Normal file
52
matrixgw_frontend/src/utils/FormUtils.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import isCidr from "is-cidr";
|
||||||
|
import type { LenConstraint } from "../api/ServerApi";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a constraint was respected or not
|
||||||
|
*
|
||||||
|
* @returns An error message appropriate for the constraint
|
||||||
|
* violation, if any, or undefined otherwise
|
||||||
|
*/
|
||||||
|
export function checkConstraint(
|
||||||
|
constraint: LenConstraint,
|
||||||
|
value: string | undefined
|
||||||
|
): string | undefined {
|
||||||
|
value = value ?? "";
|
||||||
|
if (value.length < constraint.min)
|
||||||
|
return `Please specify at least ${constraint.min} characters!`;
|
||||||
|
|
||||||
|
if (value.length > constraint.max)
|
||||||
|
return `Please specify at least ${constraint.min} characters!`;
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a number constraint was respected or not
|
||||||
|
*
|
||||||
|
* @returns An error message appropriate for the constraint
|
||||||
|
* violation, if any, or undefined otherwise
|
||||||
|
*/
|
||||||
|
export function checkNumberConstraint(
|
||||||
|
constraint: LenConstraint,
|
||||||
|
value: number
|
||||||
|
): string | undefined {
|
||||||
|
value = value ?? "";
|
||||||
|
if (value < constraint.min)
|
||||||
|
return `Value is below accepted minimum (${constraint.min})!`;
|
||||||
|
|
||||||
|
if (value > constraint.max)
|
||||||
|
return `Value is above accepted maximum (${constraint.min})!`;
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a given IP network address is valid or not
|
||||||
|
*
|
||||||
|
* @param ip The IP network to check
|
||||||
|
* @returns true if the address is valid, false otherwise
|
||||||
|
*/
|
||||||
|
export function isIPNetworkValid(ip: string): boolean {
|
||||||
|
return isCidr(ip) !== 0;
|
||||||
|
}
|
||||||
29
matrixgw_frontend/src/widgets/CopyTextChip.tsx
Normal file
29
matrixgw_frontend/src/widgets/CopyTextChip.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Chip, Tooltip } from "@mui/material";
|
||||||
|
import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider";
|
||||||
|
import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider";
|
||||||
|
|
||||||
|
export function CopyTextChip(p: { text: string }): React.ReactElement {
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
const alert = useAlert();
|
||||||
|
|
||||||
|
const copyTextToClipboard = () => {
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(p.text);
|
||||||
|
snackbar(`'${p.text}' was copied to clipboard.`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to copy text to the clipboard! ${e}`);
|
||||||
|
alert(p.text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title="Copy to clipboard">
|
||||||
|
<Chip
|
||||||
|
label={p.text}
|
||||||
|
variant="outlined"
|
||||||
|
style={{ margin: "5px" }}
|
||||||
|
onClick={copyTextToClipboard}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
matrixgw_frontend/src/widgets/forms/CheckboxInput.tsx
Normal file
23
matrixgw_frontend/src/widgets/forms/CheckboxInput.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Checkbox, FormControlLabel } from "@mui/material";
|
||||||
|
|
||||||
|
export function CheckboxInput(p: {
|
||||||
|
editable: boolean;
|
||||||
|
label: string;
|
||||||
|
checked: boolean | undefined;
|
||||||
|
onValueChange: (v: boolean) => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
disabled={!p.editable}
|
||||||
|
checked={p.checked}
|
||||||
|
onChange={(e) => {
|
||||||
|
p.onValueChange(e.target.checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={p.label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
matrixgw_frontend/src/widgets/forms/DateInput.tsx
Normal file
49
matrixgw_frontend/src/widgets/forms/DateInput.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { DateField } from "@mui/x-date-pickers";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { TextInput } from "./TextInput";
|
||||||
|
|
||||||
|
export function DateInput(p: {
|
||||||
|
editable?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
label: string;
|
||||||
|
value: number | undefined | null;
|
||||||
|
checkValue?: (s: number) => boolean;
|
||||||
|
disableFuture?: boolean;
|
||||||
|
disablePast?: boolean;
|
||||||
|
onChange: (newVal: number | undefined | null) => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const date = p.value ? dayjs.unix(p.value) : undefined;
|
||||||
|
|
||||||
|
const error = p.value && p.checkValue && !p.checkValue(p.value);
|
||||||
|
|
||||||
|
if (!p.editable)
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
{...p}
|
||||||
|
checkValue={undefined}
|
||||||
|
value={date !== undefined ? date.format("DD/MM/YYYY") : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DateField
|
||||||
|
clearable
|
||||||
|
value={date}
|
||||||
|
onChange={(v) => p.onChange(v?.unix())}
|
||||||
|
slotProps={{
|
||||||
|
textField: {
|
||||||
|
fullWidth: true,
|
||||||
|
label: p.label,
|
||||||
|
variant: "standard",
|
||||||
|
},
|
||||||
|
inputAdornment: {
|
||||||
|
variant: "standard",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
disableFuture={p.disableFuture}
|
||||||
|
disablePast={p.disablePast}
|
||||||
|
error={error === true}
|
||||||
|
format="DD/MM/YYYY"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
matrixgw_frontend/src/widgets/forms/NetworksInput.tsx
Normal file
26
matrixgw_frontend/src/widgets/forms/NetworksInput.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { isIPNetworkValid } from "../../utils/FormUtils";
|
||||||
|
import { TextInput } from "./TextInput";
|
||||||
|
|
||||||
|
function rebuildNetworksList(val?: string): string[] | undefined {
|
||||||
|
if (!val || val.trim() === "") return undefined;
|
||||||
|
|
||||||
|
return val.split(",").map((v) => v.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NetworksInput(p: {
|
||||||
|
editable?: boolean;
|
||||||
|
label: string;
|
||||||
|
value?: string[];
|
||||||
|
onChange: (n: string[] | undefined) => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const textValue = (p.value ?? []).join(", ").trim();
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
{...p}
|
||||||
|
type="string"
|
||||||
|
value={textValue}
|
||||||
|
onValueChange={(i) => p.onChange(rebuildNetworksList(i))}
|
||||||
|
checkValue={(v) => (rebuildNetworksList(v) ?? []).every(isIPNetworkValid)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
matrixgw_frontend/src/widgets/forms/TextInput.tsx
Normal file
65
matrixgw_frontend/src/widgets/forms/TextInput.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { TextField, type TextFieldVariants } from "@mui/material";
|
||||||
|
import type { LenConstraint } from "../../api/ServerApi";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text input
|
||||||
|
*/
|
||||||
|
export function TextInput(p: {
|
||||||
|
label?: string;
|
||||||
|
editable?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
value?: string;
|
||||||
|
onValueChange?: (newVal: string | undefined) => void;
|
||||||
|
size?: LenConstraint;
|
||||||
|
checkValue?: (s: string) => boolean;
|
||||||
|
multiline?: boolean;
|
||||||
|
minRows?: number;
|
||||||
|
maxRows?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
type?: React.HTMLInputTypeAttribute;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
helperText?: string;
|
||||||
|
variant?: TextFieldVariants;
|
||||||
|
}): React.ReactElement {
|
||||||
|
if (!p.editable && (p.value ?? "") === "") return <></>;
|
||||||
|
|
||||||
|
let valueError = undefined;
|
||||||
|
if (p.value && p.value.length > 0) {
|
||||||
|
if (p.size?.min && p.type !== "number" && p.value.length < p.size.min)
|
||||||
|
valueError = `Please specify at least ${p.size.min} characters !`;
|
||||||
|
if (p.checkValue && !p.checkValue(p.value)) valueError = "Invalid value!";
|
||||||
|
if (
|
||||||
|
p.type === "number" &&
|
||||||
|
p.size &&
|
||||||
|
(Number(p.value) > p.size.max || Number(p.value) < p.size.min)
|
||||||
|
)
|
||||||
|
valueError = "Invalid size range!";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
label={p.label}
|
||||||
|
required={p.required}
|
||||||
|
value={p.value ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
p.onValueChange?.(
|
||||||
|
e.target.value.length === 0 ? undefined : e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
readOnly: !p.editable,
|
||||||
|
type: p.type,
|
||||||
|
},
|
||||||
|
htmlInput: { maxLength: p.size?.max, placeholder: p.placeholder },
|
||||||
|
}}
|
||||||
|
variant={p.variant ?? "standard"}
|
||||||
|
style={p.style ?? { width: "100%", marginBottom: "15px" }}
|
||||||
|
multiline={p.multiline}
|
||||||
|
minRows={p.minRows}
|
||||||
|
maxRows={p.maxRows}
|
||||||
|
error={valueError !== undefined}
|
||||||
|
helperText={valueError ?? p.helperText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user