From 92fd933c60d1ac337dd1a6df1d111d3f554236f2 Mon Sep 17 00:00:00 2001 From: Rinsvent Date: Thu, 6 Apr 2023 16:29:08 +0700 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=B4=20?= =?UTF-8?q?=D0=BD=D0=B0=20jwt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/authentication.ts | 17 ++++ api/schema-client.ts | 39 ++++++-- api/sm/sm-client.ts | 7 -- components/elements/commands/index.tsx | 10 +- components/elements/processes/index.tsx | 23 ++--- context/token.ts | 6 ++ functions/token.ts | 120 ++++++++++++++++++++++++ hooks/use-api.ts | 30 ++++++ hooks/use-token.ts | 48 ++++++++++ pages/_app.tsx | 9 +- pages/index.tsx | 95 +++++++++++++++---- 11 files changed, 355 insertions(+), 49 deletions(-) create mode 100644 api/authentication.ts create mode 100644 context/token.ts create mode 100644 functions/token.ts create mode 100644 hooks/use-api.ts create mode 100644 hooks/use-token.ts diff --git a/api/authentication.ts b/api/authentication.ts new file mode 100644 index 0000000..574491b --- /dev/null +++ b/api/authentication.ts @@ -0,0 +1,17 @@ +export interface AuthenticationInterface { + key: string + token(): string +} + +export class BearerAuthentication implements AuthenticationInterface { + key = 'Authorization' + _token: string + + constructor(token: string) { + this._token = token + } + + token(): string { + return `Bearer ${this._token}`; + } +} \ No newline at end of file diff --git a/api/schema-client.ts b/api/schema-client.ts index 1c3a8bb..3c9b5de 100644 --- a/api/schema-client.ts +++ b/api/schema-client.ts @@ -1,3 +1,5 @@ +import {AuthenticationInterface} from "./authentication"; + export enum Method { GET = 'GET', POST = 'POST', @@ -9,10 +11,6 @@ export let hasQuery = (method: Method) => { return Method.GET === method } -export interface ClientOptions { - baseUrl: string -} - export interface SchemaInterface { url: string, method: Method @@ -25,12 +23,23 @@ export class Context { memoryKey?: string } +interface SchemaClientOptionsInterface { + baseUrl?: string + authentication?: AuthenticationInterface +} + export class SchemaClient { baseUrl: string | null = null + authentication: AuthenticationInterface | null = null context: Context | null = null memory: Record = {} + constructor(options: SchemaClientOptionsInterface) { + this.baseUrl = options.baseUrl || null + this.authentication = options.authentication || null + } + debouncing(time: number) { let context = this.grabContext() @@ -47,6 +56,10 @@ export class SchemaClient { } async send(schema: SchemaInterface, data: any) { + if (this.baseUrl === null) { + throw new Error('Base url not defined!'); + } + let context = this.context this.context = null @@ -64,12 +77,22 @@ export class SchemaClient { preparedUrl += '?' + new URLSearchParams(preparedData) } + let token = '' + if (typeof localStorage !== 'undefined') { + let tokenId = localStorage.getItem('token') + token = localStorage.getItem(tokenId || '') || '' + } + + let headers: Record = { + ...(schema.contentType ? {'Content-Type': schema.contentType} : {}), + 'X-Plugin-Token': 'passw0rd', + } + if (this.authentication) { + headers[this.authentication.key] = this.authentication.token(); + } let response = await fetch(preparedUrl, { method: schema.method.toString(), - headers: { - ...(schema.contentType ? {'Content-Type': schema.contentType} : {}), - 'X-Plugin-Token': 'passw0rd' - }, + headers, body: hasQuery(schema.method) ? null : JSON.stringify(preparedData) }); diff --git a/api/sm/sm-client.ts b/api/sm/sm-client.ts index badbcfe..77faf8c 100644 --- a/api/sm/sm-client.ts +++ b/api/sm/sm-client.ts @@ -17,11 +17,7 @@ import stopProcessSchema from "./schemas/stop-process"; import killProcessSchema from "./schemas/kill-process"; import commandSchema from "./schemas/command"; -let baseUrl = typeof location !== 'undefined' && location.origin.includes('.wallester.') ? location.origin : 'http://fmw.sipachev.sv' - export class SMClient extends SchemaClient { - baseUrl = baseUrl - async getCommands(): Promise> { let { responseData, headers } = await this.send(commandsSchema, {}) return { @@ -104,6 +100,3 @@ export class SMClient extends SchemaClient { } } } - -export let smClient = new SMClient -export default smClient diff --git a/components/elements/commands/index.tsx b/components/elements/commands/index.tsx index 9014b1d..b3fa494 100644 --- a/components/elements/commands/index.tsx +++ b/components/elements/commands/index.tsx @@ -1,14 +1,13 @@ import styles from './styles.module.css' -import {Table, TableBody, TableCell, TableHead, TableRow, IconButton, Autocomplete, TextField} from "@mui/material"; +import {IconButton, Autocomplete, TextField} from "@mui/material"; import Command from "./elements/command"; import {useContext, useEffect, useState} from "react"; -import PlayCircleOutline from '@mui/icons-material/PlayCircleOutline'; import ConfirmDialog from "../confirm-dialog"; import TabContext from "../../../context/tab"; import {TabEnum} from "../../../pages"; -import smClient from "../../../api/sm/sm-client"; import Send from "@mui/icons-material/Send" import {CommandInterface} from "../../../api/sm/responses/comamnds"; +import {useApi} from "../../../hooks/use-api"; export default function Commands() { const {setTab} = useContext(TabContext) @@ -18,9 +17,10 @@ export default function Commands() { const [argumentList, setArgumentList] = useState>({}); const [value, setValue] = useState(''); const [open, setOpen] = useState(false); + const api = useApi() let refreshCommands = async () => { - const { data: commands } = await smClient.useMemory().getCommands() + const { data: commands } = await api.useMemory().getCommands() setCommands(commands) } @@ -48,7 +48,7 @@ export default function Commands() { return } lock = true - await smClient.runCommand({ + await api.runCommand({ commandName: selectedCommand.name, options: optionList, arguments: argumentList, diff --git a/components/elements/processes/index.tsx b/components/elements/processes/index.tsx index 1f552c2..cce0eb0 100644 --- a/components/elements/processes/index.tsx +++ b/components/elements/processes/index.tsx @@ -25,11 +25,11 @@ import { TablePagination, Autocomplete } from "@mui/material" import ConfirmDialog from "../confirm-dialog"; -import smClient from "../../../api/sm/sm-client"; import {ProcessInterface, Status} from "../../../api/sm/responses/processes"; import Command from "../commands/elements/command"; import {CommandInterface} from "../../../api/sm/responses/comamnds"; import Grid from '@mui/material/Grid'; +import {useApi} from "../../../hooks/use-api"; enum Action { Run, @@ -54,11 +54,12 @@ export default function Processes() { const [selectedProcess, setSelectedProcess] = useState(null); const [optionList, setOptionList] = useState>({}); const [argumentList, setArgumentList] = useState>({}); + const api = useApi() let variants = commands.map((command: CommandInterface, index: number) => command.name); let refreshCommands = async () => { - const { data: commands } = await smClient.useMemory().getCommands() + const { data: commands } = await api.useMemory().getCommands() setCommands(commands) } @@ -80,7 +81,7 @@ export default function Processes() { if (!!name) { data['name'] = name } - const { data: processes, headers } = await smClient.getProcesses({ + const { data: processes, headers } = await api.getProcesses({ ...data, page: page + 1, limit: 20, @@ -91,7 +92,7 @@ export default function Processes() { refreshLock = false } let output = async (process: ProcessInterface) => { - const { data: output } = await smClient.getProcessOutput({ + const { data: output } = await api.getProcessOutput({ id: process.id }) @@ -136,7 +137,7 @@ export default function Processes() { lock = true if (action === Action.Run) { - await smClient.runCommand({ + await api.runCommand({ commandName: selectedProcess.name, options: optionList, arguments: argumentList, @@ -145,7 +146,7 @@ export default function Processes() { } if (action === Action.Repeat) { - await smClient.repeatProcess({ + await api.repeatProcess({ id: selectedProcess.id, requestId: dialogId }) @@ -278,7 +279,7 @@ export default function Processes() { modifyCallback={async () => { setAction(Action.Run) setModalLoading(true) - let {data: command} = await smClient.getCommand(selectedProcess.name) + let {data: command} = await api.getCommand(selectedProcess.name) setCommand(command) setModalLoading(false) }} @@ -303,10 +304,10 @@ export default function Processes() { title={`Are you sure?`} open={ !!action && ([Action.Play, Action.Pause, Action.Stop, Action.Kill].indexOf(action) > -1) } agreeCallback={async () => { - Action.Play === action && await smClient.playProcess(selectedProcess.id) - Action.Pause === action && await smClient.pauseProcess(selectedProcess.id) - Action.Stop === action && await smClient.stopProcess(selectedProcess.id) - Action.Kill === action && await smClient.killProcess(selectedProcess.id) + Action.Play === action && await api.playProcess(selectedProcess.id) + Action.Pause === action && await api.pauseProcess(selectedProcess.id) + Action.Stop === action && await api.stopProcess(selectedProcess.id) + Action.Kill === action && await api.killProcess(selectedProcess.id) setAction(null) }} closeCallback={() => {setAction(null)}}> diff --git a/context/token.ts b/context/token.ts new file mode 100644 index 0000000..ea6e9de --- /dev/null +++ b/context/token.ts @@ -0,0 +1,6 @@ +import React from 'react' +import {UseTokenInterface} from "../hooks/use-token" + +const Context = React.createContext({} as UseTokenInterface) +export const Provider = Context.Provider +export default Context diff --git a/functions/token.ts b/functions/token.ts new file mode 100644 index 0000000..8b0b9b6 --- /dev/null +++ b/functions/token.ts @@ -0,0 +1,120 @@ + + +export interface TokenData { + payload: T + headers: Record +} + +const isClient = () => { + return typeof window !== 'undefined' +} + +export const storeToken = (token: string) => { + if (!isClient()) { + return; + } + let tokenData = grabTokenData(token) + let tokenId = tokenData?.payload?.id; + if (!tokenId) { + return; + } + + let tokens: string | null = window.localStorage.getItem('tokens') + let tokensData: string[] = JSON.parse(tokens || '[]'); + if (!tokensData) { + return + } + window.localStorage.setItem('token', tokenId) + window.localStorage.setItem(tokenId, token) + tokensData.push(tokenId) + window.localStorage.setItem('tokens', JSON.stringify(tokensData)) +} + +export const cleanToken = (id: string) => { + if (!isClient()) { + return; + } + let tokens: string | null = window.localStorage.getItem('tokens') + let tokensData: string[] = JSON.parse(tokens || '[]'); + if (!tokensData) { + return + } + + tokensData = tokensData.filter((storedTokenId: string) => storedTokenId !== id) + window.localStorage.setItem('tokens', JSON.stringify(tokensData)) + window.localStorage.removeItem(id) + + // Чистим текущий токен + let activeTokenId: string | null = window.localStorage.getItem('token') + if (id === activeTokenId) { + window.localStorage.removeItem('token') + } +} + +export const selectToken = (id: string) => { + if (!isClient()) { + return; + } + window.localStorage.setItem('token', id) +} + +export const grabTokenData = (token: string): TokenData|null => { + if (!isClient()) { + return null + } + const parts = token.split('.') + let payload = 3 === parts.length && parts[1] ? JSON.parse(window.atob(parts[1])) : null + let headers = 3 === parts.length && parts[0] ? JSON.parse(window.atob(parts[0])) : null + if (!payload || !headers){ + return null + } + return { + payload, + headers, + }; +} + +export const grabRawToken = (): string | null => { + if (!isClient()) { + return null + } + const tokenId = window.localStorage.getItem('token') + if (!tokenId) { + return null + } + return window.localStorage.getItem(tokenId) +} + +export const grabToken = (): TokenData | null => { + const token = grabRawToken() + if (!token) { + return null + } + return grabTokenData(token); +} + +export const grabTokens = (): TokenData[] => { + if (!isClient()) { + return [] + } + const storedTokens = window.localStorage.getItem('tokens') + let tokens: string[] = JSON.parse(storedTokens || '[]'); + if (!tokens) { + return [] + } + + let result: TokenData[] = [] + + tokens.forEach((tokenId) => { + let token = window.localStorage.getItem(tokenId) + if (!token) { + return true + } + let tokenData = grabTokenData(token) + if (tokenData) { + result.push(tokenData) + } + }) + + return result +} diff --git a/hooks/use-api.ts b/hooks/use-api.ts new file mode 100644 index 0000000..b50a811 --- /dev/null +++ b/hooks/use-api.ts @@ -0,0 +1,30 @@ +import {useContext, useEffect, useState} from 'react' +import {SMClient} from "../api/sm/sm-client"; +import Context from "../context/token"; +import {Token} from "./use-token"; +import {BearerAuthentication} from "../api/authentication"; +import {grabRawToken} from "../functions/token"; + +const grabClient = (token: Token | null, rawToken: string | null): SMClient => { + let authentication = {} + if (rawToken) { + authentication = { + authentication: new BearerAuthentication(rawToken) + } + } + return new SMClient({ + baseUrl: 'http://' + (token?.host || 'localhost'), + ...authentication + }) +} + +export function useApi(): SMClient { + let {token} = useContext(Context) + let [client, setClient] = useState(grabClient(token, grabRawToken())) + + useEffect(() => { + setClient(grabClient(token, grabRawToken())) + }, [token]) + + return client +} \ No newline at end of file diff --git a/hooks/use-token.ts b/hooks/use-token.ts new file mode 100644 index 0000000..85717a0 --- /dev/null +++ b/hooks/use-token.ts @@ -0,0 +1,48 @@ +import {useState, useEffect} from 'react' +import {cleanToken, grabToken, grabTokens, selectToken, storeToken} from "../functions/token"; + +export class Token { + id!: string + host!: string + permissions!: string[] +} + +export interface UseTokenInterface { + token: Token|null + tokens: Token[] + switchToken(tokenId: string): void + deleteToken(tokenId: string): void + addToken(tokenId: string): void +} + +export function useToken(): UseTokenInterface { + const [token, setToken] = useState(null); + const [tokens, setTokens] = useState([]); + useEffect(() => { + setToken(grabToken()?.payload) + + let tokens = grabTokens().map((tokenData) => tokenData.payload) + setTokens(tokens) + + return () => { + + } + }, []) + + const switchToken = (tokenId: string) => { + selectToken(tokenId) + setToken(grabToken()?.payload) + } + + const deleteToken = (tokenId: string) => { + cleanToken(tokenId) + setToken(grabToken()?.payload) + } + + const addToken = (token: string) => { + storeToken(token) + setToken(grabToken()?.payload) + } + + return {token, tokens, switchToken, deleteToken, addToken} +} \ No newline at end of file diff --git a/pages/_app.tsx b/pages/_app.tsx index c055f25..179e2fe 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,6 +1,13 @@ import '../styles/globals.css' import type { AppProps } from 'next/app' +import {Provider} from "../context/token"; +import {useToken} from "../hooks/use-token"; export default function App({ Component, pageProps }: AppProps) { - return + let {token, tokens, switchToken, deleteToken, addToken} = useToken() + return ( + + + + ) } diff --git a/pages/index.tsx b/pages/index.tsx index 544595c..1550d52 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,10 +1,12 @@ import Head from 'next/head' import styles from '../styles/Home.module.css' import Processes from "../components/elements/processes"; -import {Tabs, Tab} from '@mui/material'; -import {useState} from "react"; +import {Tabs, Tab, MenuItem, Select, InputLabel, TextareaAutosize, Button} from '@mui/material'; +import {useContext, useEffect, useState} from "react"; import Commands from "../components/elements/commands"; import {Provider as TabProvider} from '../context/tab' +import Context from "../context/token"; + export enum TabEnum { Commands, @@ -12,8 +14,42 @@ export enum TabEnum { } export default function Home() { + let {token, tokens, addToken} = useContext(Context) const [tab, setTab] = useState(TabEnum.Processes); const handleChange = (event: any, tab: number) => setTab(tab) + + // useEffect(() => { + // const storedToken = grabToken() + // setToken(storedToken) + // const storedTokens = grabTokens() + // setTokens(storedTokens) + // }, []) + + let onTokenInput = (event: any) => { + + } + + if (!token) { + return ( + <> + + System monitoring + + + + +
+

Для дальнейшей работы добавьте JWT токен

+ addToken(event.target.value)} + /> +
+ + ) + } + return ( <> @@ -23,21 +59,46 @@ export default function Home() {
- - - - - - {tab === TabEnum.Commands && } - {tab === TabEnum.Processes && } - + {tokens.length > 0 && <> + Tokens + + } + + + {token && <> + + + + + + {tab === TabEnum.Commands && } + {tab === TabEnum.Processes && } + + }
)