From 0da4b9561b93bdd0846da563c9c1b7ae32c01c5e Mon Sep 17 00:00:00 2001 From: Tim Pechersky Date: Tue, 21 Sep 2021 16:58:13 +0200 Subject: [PATCH] frontend changes --- frontend/pages/account/tokens.js | 97 +++++++++++++++++ frontend/src/Theme/Heading.js | 57 ++++++++++ frontend/src/Theme/theme.js | 2 + frontend/src/components/AccountIconButton.js | 3 + frontend/src/components/NewTokenTr.js | 85 +++++++++++++++ frontend/src/components/TokenList.js | 101 +++++++++++++++++ frontend/src/components/TokenRequest.js | 109 +++++++++++++++++++ frontend/src/components/TokensList.js | 88 +++++++++++++++ frontend/src/core/hooks/index.js | 4 +- frontend/src/core/hooks/useTokens.js | 40 +++++++ frontend/src/core/services/auth.service.js | 25 +++++ 11 files changed, 610 insertions(+), 1 deletion(-) create mode 100644 frontend/pages/account/tokens.js create mode 100644 frontend/src/Theme/Heading.js create mode 100644 frontend/src/components/NewTokenTr.js create mode 100644 frontend/src/components/TokenList.js create mode 100644 frontend/src/components/TokenRequest.js create mode 100644 frontend/src/components/TokensList.js create mode 100644 frontend/src/core/hooks/useTokens.js diff --git a/frontend/pages/account/tokens.js b/frontend/pages/account/tokens.js new file mode 100644 index 00000000..03718d9f --- /dev/null +++ b/frontend/pages/account/tokens.js @@ -0,0 +1,97 @@ +import React, { useState, useEffect, useLayoutEffect } from "react"; +import TokensList from "../../src/components/TokensList"; +import TokenRequest from "../../src/components/TokenRequest"; +import { useTokens } from "../../src/core/hooks"; +import { + VStack, + Box, + Center, + Spinner, + ScaleFade, + Button, + Heading, +} from "@chakra-ui/react"; +import { getLayout } from "../../src/layouts/AccountLayout"; + +const Tokens = () => { + const [modal, toggleModal] = useState(null); + const [newToken, setNewToken] = useState(null); + const [tokens, setTokens] = useState(); + const { list, update, revoke, isLoading, data } = useTokens(); + + useEffect(() => { + list(); + //eslint-disable-next-line + }, []); + + useLayoutEffect(() => { + if (newToken) { + const newData = { ...tokens }; + newData.token.push(newToken); + setTokens(newData); + setNewToken(null); + } + }, [newToken, list, data, tokens]); + + useLayoutEffect(() => { + if (data?.data?.user_id) { + setTokens(data.data); + } + }, [data?.data, isLoading]); + + useEffect(() => { + document.title = `Tokens`; + }, []); + + return ( + + {isLoading && !tokens ? ( +
+
+ ) : ( + + My access tokens + +
+ + {!modal ? ( + + + + ) : ( + + + + )} + +
+ +
+
+ )} +
+ ); +}; + +Tokens.getLayout = getLayout; +export default Tokens; diff --git a/frontend/src/Theme/Heading.js b/frontend/src/Theme/Heading.js new file mode 100644 index 00000000..c0b6a28e --- /dev/null +++ b/frontend/src/Theme/Heading.js @@ -0,0 +1,57 @@ +const baseStyle = { + fontFamily: "heading", + fontWeight: "bold", +}; + +const sizes = { + "4xl": { + fontSize: ["6xl", null, "7xl"], + lineHeight: 1, + }, + "3xl": { + fontSize: ["5xl", null, "6xl"], + lineHeight: 1, + }, + "2xl": { + fontSize: ["4xl", null, "5xl"], + lineHeight: [1.2, null, 1], + }, + xl: { + fontSize: ["3xl", null, "4xl"], + lineHeight: [1.33, null, 1.2], + }, + lg: { + fontSize: ["2xl", null, "3xl"], + lineHeight: [1.33, null, 1.2], + }, + md: { fontSize: "xl", lineHeight: 1.2 }, + sm: { fontSize: "md", lineHeight: 1.2 }, + xs: { fontSize: "sm", lineHeight: 1.2 }, +}; + +const defaultProps = { + size: "xl", +}; + +const variantTokensScreen = (props) => { + const { colorScheme: c } = props; + return { + as: "h2", + pt: 2, + mb: 4, + borderBottom: "solid", + borderColor: `${c}.50`, + borderBottomWidth: "2px", + }; +}; + +const variants = { + tokensScreen: variantTokensScreen, +}; + +export default { + baseStyle, + sizes, + defaultProps, + variants, +}; diff --git a/frontend/src/Theme/theme.js b/frontend/src/Theme/theme.js index 40f70a1f..fbf3a407 100644 --- a/frontend/src/Theme/theme.js +++ b/frontend/src/Theme/theme.js @@ -10,6 +10,7 @@ import Checkbox from "./Checkbox"; import Table from "./Table"; import Tooltip from "./Tooltip"; import Spinner from "./Spinner"; +import Heading from "./Heading"; import { createBreakpoints } from "@chakra-ui/theme-tools"; const breakpointsCustom = createBreakpoints({ @@ -57,6 +58,7 @@ const theme = extendTheme({ Table, Spinner, Tooltip, + Heading, }, fonts: { diff --git a/frontend/src/components/AccountIconButton.js b/frontend/src/components/AccountIconButton.js index 08dad6f3..4dc74bb5 100644 --- a/frontend/src/components/AccountIconButton.js +++ b/frontend/src/components/AccountIconButton.js @@ -35,6 +35,9 @@ const AccountIconButton = (props) => { Security + + Access tokens + { + const inputRef = useRef(null); + useEffect(() => { + if (isOpen) { + //without timeout input is not catching focus on chrome and firefox.. + //probably because it is hidden within accordion + setTimeout(() => { + inputRef.current.focus(); + }, 100); + } + }, [inputRef, isOpen]); + + return ( + + {isOpen && ( + + New Token: + + + + { + register(e, { required: "app name is required" }); + inputRef.current = e; + }} + /> + + + {errors.appName && errors.appName.message} + + + + + + + { + register(e, { required: "app name is required" }); + }} + /> + + + {errors.appVersion && errors.appVersion.message} + + + + + + toggleSelf(false)} + icon={} + /> + + + )} + + ); +}; + +export default NewTokenTr; diff --git a/frontend/src/components/TokenList.js b/frontend/src/components/TokenList.js new file mode 100644 index 00000000..2f3aaaa5 --- /dev/null +++ b/frontend/src/components/TokenList.js @@ -0,0 +1,101 @@ +import React from "react"; +import { IconButton } from "@chakra-ui/react"; +import { + Table, + Th, + Td, + Tr, + Thead, + Tbody, + Text, + Center, + Spinner, +} from "@chakra-ui/react"; +import { DeleteIcon } from "@chakra-ui/icons"; +import { CopyButton, ConfirmationRequest, NewTokenTr } from "."; +import { useForm } from "react-hook-form"; + +const TokenList = ({ + tokens, + revoke, + isLoading, + isNewTokenOpen, + toggleNewToken, + createToken, + journalName, +}) => { + const { register, handleSubmit, errors } = useForm(); + if (isLoading) + return ( +
+ +
+ ); + + const handleTokenSubmit = ({ appName, appVersion }) => { + createToken({ appName, appVersion }).then(() => toggleNewToken(false)); + }; + + return ( +
+ + + + + + + + + + + {tokens.map((token, idx) => { + return ( + + + + + + + ); + })} + + + +
TokenApp NameApp versionAction
+ {token.restricted_token_id} + {token.app_name}{token.app_version} + revoke(token.restricted_token_id)} + > + } + /> + +
+ {tokens.length < 1 && ( +
+ Create Usage report tokens here +
+ )} +
+ ); +}; +export default TokenList; diff --git a/frontend/src/components/TokenRequest.js b/frontend/src/components/TokenRequest.js new file mode 100644 index 00000000..9f0eacf7 --- /dev/null +++ b/frontend/src/components/TokenRequest.js @@ -0,0 +1,109 @@ +import React from "react"; +import { + Box, + InputGroup, + InputLeftElement, + FormControl, + FormErrorMessage, + HStack, + Button, + InputRightElement, + Input, + Icon, +} from "@chakra-ui/react"; +import { CloseIcon } from "@chakra-ui/icons"; +import { useEffect, useState, useRef } from "react"; + +import { useForm } from "react-hook-form"; +import { useUser, useTokens } from "../core/hooks"; + +const TokenRequest = ({ newToken, toggle }) => { + const { user } = useUser(); + const { createToken } = useTokens(); + const { handleSubmit, errors, register } = useForm(); + const [showPassword, setShowPassword] = useState("password"); + + const togglePassword = () => { + if (showPassword === "password") { + setShowPassword("text"); + } else { + setShowPassword("password"); + } + }; + + const PasswordRef = useRef(); + + useEffect(() => { + if (PasswordRef.current) { + PasswordRef.current.focus(); + } + }, [PasswordRef]); + + useEffect(() => { + if (createToken.data?.data) { + newToken(createToken.data.data); + toggle(null); + } + }, [createToken.data, newToken, toggle]); + + const formStyle = { + display: "flex", + flexWrap: "wrap", + minWidth: "100px", + flexFlow: "row wrap-reverse", + aligntContent: "flex-end", + }; + + if (!user) return ""; //loading... + + return ( + +
+ + + + + + + + + { + register(e, { required: "Password is required!" }); + PasswordRef.current = e; + }} + /> + toggle(null)}> + + + + + {errors.password && errors.password.message} + + + + +
+
+ ); +}; +export default TokenRequest; diff --git a/frontend/src/components/TokensList.js b/frontend/src/components/TokensList.js new file mode 100644 index 00000000..21f3d475 --- /dev/null +++ b/frontend/src/components/TokensList.js @@ -0,0 +1,88 @@ +import React from "react"; +import { Skeleton, IconButton } from "@chakra-ui/react"; +import { + Table, + Th, + Td, + Tr, + Thead, + Tbody, + Editable, + EditableInput, + EditablePreview, +} from "@chakra-ui/react"; +import { DeleteIcon } from "@chakra-ui/icons"; +import moment from "moment"; +import CopyButton from "./CopyButton"; + +const List = ({ data, revoke, isLoading, updateCallback }) => { + const userToken = localStorage.getItem("BUGOUT_ACCESS_TOKEN"); + + if (data) { + return ( + + + + + + + + + + + {data.token.map((token) => { + if (token.active) { + if (userToken !== token.id) { + return ( + + + + + + + ); + } else return null; + } else return null; + })} + +
TokenDate CreatedNoteActions
+ {token.id} + {moment(token.created_at).format("L")} + + updateCallback({ token: token.id, note: nextValue }) + } + > + + + + + revoke(token.id)} + icon={} + /> +
+ ); + } else if (isLoading) { + return ; + } else { + return ""; + } +}; +export default List; diff --git a/frontend/src/core/hooks/index.js b/frontend/src/core/hooks/index.js index 5625f907..0327ba50 100644 --- a/frontend/src/core/hooks/index.js +++ b/frontend/src/core/hooks/index.js @@ -1,4 +1,4 @@ -export { queryCacheProps as hookCommon } from "./hookCommon"; +export { default as hookCommon } from "./hookCommon"; export { default as useAuthResultHandler } from "./useAuthResultHandler"; export { default as useChangePassword } from "./useChangePassword"; export { default as useClientID } from "./useClientID"; @@ -15,8 +15,10 @@ export { default as useResetPassword } from "./useResetPassword"; export { default as useRouter } from "./useRouter"; export { default as useSignUp } from "./useSignUp"; export { default as useStorage } from "./useStorage"; +export { default as useStream } from "./useStream"; export { default as useStripe } from "./useStripe"; export { default as useSubscriptions } from "./useSubscriptions"; export { default as useToast } from "./useToast"; +export { default as useTokens } from "./useTokens"; export { default as useTxInfo } from "./useTxInfo"; export { default as useUser } from "./useUser"; diff --git a/frontend/src/core/hooks/useTokens.js b/frontend/src/core/hooks/useTokens.js new file mode 100644 index 00000000..559dfdc5 --- /dev/null +++ b/frontend/src/core/hooks/useTokens.js @@ -0,0 +1,40 @@ +import { useMutation } from "react-query"; +import { AuthService } from "../services"; + +const useTokens = () => { + const { + mutate: list, + isLoading, + error, + data, + } = useMutation(AuthService.getTokenList); + const { mutate: revoke } = useMutation(AuthService.revokeToken, { + onSuccess: () => { + list(); + }, + }); + + const { mutate: update } = useMutation(AuthService.updateToken, { + onSuccess: () => { + list(); + }, + }); + + const createToken = useMutation(AuthService.login, { + onSuccess: () => { + list(); + }, + }); + + return { + createToken, + list, + update, + revoke, + isLoading, + data, + error, + }; +}; + +export default useTokens; diff --git a/frontend/src/core/services/auth.service.js b/frontend/src/core/services/auth.service.js index bdc34303..c6778aa1 100644 --- a/frontend/src/core/services/auth.service.js +++ b/frontend/src/core/services/auth.service.js @@ -74,3 +74,28 @@ export const changePassword = ({ currentPassword, newPassword }) => { data, }); }; + +export const getTokenList = () => { + return http({ + method: "GET", + url: `${AUTH_URL}/tokens`, + }); +}; + +export const updateToken = ({ note, token }) => { + const data = new FormData(); + data.append("token_note", note); + data.append("access_token", token); + return http({ + method: "PUT", + url: `${AUTH_URL}/token`, + data, + }); +}; + +export const revokeToken = (token) => { + return http({ + method: "POST", + url: `${AUTH_URL}/revoke/${token}`, + }); +};