From 2a103b3209b31cbf4065ea546147e3e084104240 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 23 Feb 2023 15:50:31 +0000 Subject: [PATCH 01/10] add tokens switch --- .../TokensPage/TokensPage.tsx | 36 +++++++++++++++++-- .../UserSettingsPage/TokensPage/hooks.ts | 30 +++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index 34612c180a6a9..aee8854ca24e7 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -5,9 +5,16 @@ import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" import { Typography } from "components/Typography/Typography" import makeStyles from "@material-ui/core/styles/makeStyles" import { useTranslation } from "react-i18next" -import { useTokensData, useDeleteToken } from "./hooks" +import { + useTokensData, + useDeleteToken, + useCheckTokenPermissions, +} from "./hooks" import { displaySuccess, displayError } from "components/GlobalSnackbar/utils" import { getErrorMessage } from "api/errors" +import Switch from "@material-ui/core/Switch" +import FormGroup from "@material-ui/core/FormGroup" +import FormControlLabel from "@material-ui/core/FormControlLabel" export const TokensPage: FC> = () => { const styles = useStyles() @@ -15,6 +22,8 @@ export const TokensPage: FC> = () => { const [tokenIdToDelete, setTokenIdToDelete] = useState( undefined, ) + const [viewAllTokens, setViewAllTokens] = useState(false) + const { data: perms } = useCheckTokenPermissions() const { data: tokens, @@ -23,7 +32,7 @@ export const TokensPage: FC> = () => { isFetched, queryKey, } = useTokensData({ - include_all: true, + include_all: viewAllTokens, }) const { mutate: deleteToken, isLoading: isDeleting } = @@ -59,6 +68,22 @@ export const TokensPage: FC> = () => { return ( <>
+ + {perms?.readAllApiKeys && ( + setViewAllTokens(!viewAllTokens)} + name="viewAllTokens" + color="primary" + /> + } + label={t("toggleLabel")} + /> + )} + ({ justifyContent: "end", marginBottom: "10px", }, + selectAllSwitch: { + // decrease the hover state on the switch + // so that it isn't hidden behind the container + "& .MuiIconButton-root": { + padding: "8px", + }, + }, })) export default TokensPage diff --git a/site/src/pages/UserSettingsPage/TokensPage/hooks.ts b/site/src/pages/UserSettingsPage/TokensPage/hooks.ts index a739d95bf98b4..77acb76087c38 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/hooks.ts +++ b/site/src/pages/UserSettingsPage/TokensPage/hooks.ts @@ -4,9 +4,36 @@ import { useQueryClient, QueryKey, } from "@tanstack/react-query" -import { getTokens, deleteAPIKey } from "api/api" +import { getTokens, deleteAPIKey, checkAuthorization } from "api/api" import { TokensFilter } from "api/typesGenerated" +// Owners have the ability to read all API tokens, +// whereas members can only see the tokens they have created. +// We check permissions here to determine whether to display the +// 'View All' switch on the TokensPage +export const useCheckTokenPermissions = () => { + const queryKey = ["auth"] + const params = { + checks: { + readAllApiKeys: { + object: { + resource_type: "api_key", + }, + action: "read", + }, + }, + } + const result = useQuery({ + queryKey, + queryFn: () => checkAuthorization(params), + }) + + return { + ...result, + } +} + +// Load all tokens export const useTokensData = ({ include_all }: TokensFilter) => { const queryKey = ["tokens", include_all] const result = useQuery({ @@ -23,6 +50,7 @@ export const useTokensData = ({ include_all }: TokensFilter) => { } } +// Delete a token export const useDeleteToken = (queryKey: QueryKey) => { const queryClient = useQueryClient() From 640274509402d0bbe2fd894aad56b644f90e08f8 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 23 Feb 2023 16:30:13 +0000 Subject: [PATCH 02/10] reorged TokensPage --- .../TokensPage/TokensPage.tsx | 108 ++++-------------- .../components/ConfirmDeleteDialog.tsx | 59 ++++++++++ .../TokensPage/components/TokensSwitch.tsx | 48 ++++++++ .../TokensPage/components/index.ts | 2 + 4 files changed, 129 insertions(+), 88 deletions(-) create mode 100644 site/src/pages/UserSettingsPage/TokensPage/components/ConfirmDeleteDialog.tsx create mode 100644 site/src/pages/UserSettingsPage/TokensPage/components/TokensSwitch.tsx create mode 100644 site/src/pages/UserSettingsPage/TokensPage/components/index.ts diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index aee8854ca24e7..d198894dd5e0f 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -1,24 +1,22 @@ import { FC, PropsWithChildren, useState } from "react" -import { Section } from "../../../components/SettingsLayout/Section" +import { Section } from "components/SettingsLayout/Section" import { TokensPageView } from "./TokensPageView" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import { Typography } from "components/Typography/Typography" import makeStyles from "@material-ui/core/styles/makeStyles" import { useTranslation } from "react-i18next" -import { - useTokensData, - useDeleteToken, - useCheckTokenPermissions, -} from "./hooks" -import { displaySuccess, displayError } from "components/GlobalSnackbar/utils" -import { getErrorMessage } from "api/errors" -import Switch from "@material-ui/core/Switch" -import FormGroup from "@material-ui/core/FormGroup" -import FormControlLabel from "@material-ui/core/FormControlLabel" +import { useTokensData, useCheckTokenPermissions } from "./hooks" +import { TokensSwitch, ConfirmDeleteDialog } from "./components" export const TokensPage: FC> = () => { const styles = useStyles() const { t } = useTranslation("tokensPage") + + const description = ( + <> + {t("description")}{" "} + coder tokens create command. + + ) + const [tokenIdToDelete, setTokenIdToDelete] = useState( undefined, ) @@ -35,55 +33,14 @@ export const TokensPage: FC> = () => { include_all: viewAllTokens, }) - const { mutate: deleteToken, isLoading: isDeleting } = - useDeleteToken(queryKey) - - const onDeleteSuccess = () => { - displaySuccess(t("deleteToken.deleteSuccess")) - setTokenIdToDelete(undefined) - } - - const onDeleteError = (error: unknown) => { - const message = getErrorMessage(error, t("deleteToken.deleteFailure")) - displayError(message) - setTokenIdToDelete(undefined) - } - - const description = ( - <> - {t("description")}{" "} - coder tokens create command. - - ) - - const content = ( - - {t("deleteToken.deleteCaption")} -
-
- {tokenIdToDelete} -
- ) - return ( <>
- - {perms?.readAllApiKeys && ( - setViewAllTokens(!viewAllTokens)} - name="viewAllTokens" - color="primary" - /> - } - label={t("toggleLabel")} - /> - )} - + > = () => { }} />
- - { - if (!tokenIdToDelete) { - return - } - deleteToken(tokenIdToDelete, { - onError: onDeleteError, - onSuccess: onDeleteSuccess, - }) - }} - onClose={() => { - setTokenIdToDelete(undefined) - }} + ) @@ -125,17 +68,6 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.primary, borderRadius: 2, }, - formRow: { - justifyContent: "end", - marginBottom: "10px", - }, - selectAllSwitch: { - // decrease the hover state on the switch - // so that it isn't hidden behind the container - "& .MuiIconButton-root": { - padding: "8px", - }, - }, })) export default TokensPage diff --git a/site/src/pages/UserSettingsPage/TokensPage/components/ConfirmDeleteDialog.tsx b/site/src/pages/UserSettingsPage/TokensPage/components/ConfirmDeleteDialog.tsx new file mode 100644 index 0000000000000..b9c6c483bb6b0 --- /dev/null +++ b/site/src/pages/UserSettingsPage/TokensPage/components/ConfirmDeleteDialog.tsx @@ -0,0 +1,59 @@ +import { FC } from "react" +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" +import { useTranslation } from "react-i18next" +import { Typography } from "components/Typography/Typography" +import { useDeleteToken } from "../hooks" +import { displaySuccess, displayError } from "components/GlobalSnackbar/utils" +import { getErrorMessage } from "api/errors" + +export const ConfirmDeleteDialog: FC<{ + queryKey: (string | boolean)[] + tokenId: string | undefined + setTokenId: (arg: string | undefined) => void +}> = ({ queryKey, tokenId, setTokenId }) => { + const { t } = useTranslation("tokensPage") + + const content = ( + + {t("deleteToken.deleteCaption")} +
+
+ {tokenId} +
+ ) + + const { mutate: deleteToken, isLoading: isDeleting } = + useDeleteToken(queryKey) + + const onDeleteSuccess = () => { + displaySuccess(t("deleteToken.deleteSuccess")) + setTokenId(undefined) + } + + const onDeleteError = (error: unknown) => { + const message = getErrorMessage(error, t("deleteToken.deleteFailure")) + displayError(message) + setTokenId(undefined) + } + + return ( + { + if (!tokenId) { + return + } + deleteToken(tokenId, { + onError: onDeleteError, + onSuccess: onDeleteSuccess, + }) + }} + onClose={() => { + setTokenId(undefined) + }} + /> + ) +} diff --git a/site/src/pages/UserSettingsPage/TokensPage/components/TokensSwitch.tsx b/site/src/pages/UserSettingsPage/TokensPage/components/TokensSwitch.tsx new file mode 100644 index 0000000000000..516559f05b597 --- /dev/null +++ b/site/src/pages/UserSettingsPage/TokensPage/components/TokensSwitch.tsx @@ -0,0 +1,48 @@ +import { FC } from "react" +import Switch from "@material-ui/core/Switch" +import FormGroup from "@material-ui/core/FormGroup" +import FormControlLabel from "@material-ui/core/FormControlLabel" +import makeStyles from "@material-ui/core/styles/makeStyles" +import { useTranslation } from "react-i18next" + +export const TokensSwitch: FC<{ + hasReadAll: boolean + viewAllTokens: boolean + setViewAllTokens: (arg: boolean) => void +}> = ({ hasReadAll, viewAllTokens, setViewAllTokens }) => { + const styles = useStyles() + const { t } = useTranslation("tokensPage") + + return ( + + {hasReadAll && ( + setViewAllTokens(!viewAllTokens)} + name="viewAllTokens" + color="primary" + /> + } + label={t("toggleLabel")} + /> + )} + + ) +} + +const useStyles = makeStyles(() => ({ + formRow: { + justifyContent: "end", + marginBottom: "10px", + }, + selectAllSwitch: { + // decrease the hover state on the switch + // so that it isn't hidden behind the container + "& .MuiIconButton-root": { + padding: "8px", + }, + }, +})) diff --git a/site/src/pages/UserSettingsPage/TokensPage/components/index.ts b/site/src/pages/UserSettingsPage/TokensPage/components/index.ts new file mode 100644 index 0000000000000..e58a46323c61a --- /dev/null +++ b/site/src/pages/UserSettingsPage/TokensPage/components/index.ts @@ -0,0 +1,2 @@ +export { ConfirmDeleteDialog } from "./ConfirmDeleteDialog" +export { TokensSwitch } from "./TokensSwitch" From 340a9d15c9e0322a31518229d25169421328d172 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 23 Feb 2023 16:56:25 +0000 Subject: [PATCH 03/10] using Trans component for description --- site/src/i18n/en/tokensPage.json | 2 +- .../TokensPage/TokensPage.tsx | 32 ++++++++++++------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/site/src/i18n/en/tokensPage.json b/site/src/i18n/en/tokensPage.json index 4012a2cb5c4ae..b071c1b44e979 100644 --- a/site/src/i18n/en/tokensPage.json +++ b/site/src/i18n/en/tokensPage.json @@ -1,6 +1,6 @@ { "title": "Tokens", - "description": "Tokens are used to authenticate with the Coder API. You can create a token with the Coder CLI using the ", + "description": "Tokens are used to authenticate with the Coder API. You can create a token with the Coder CLI using the <1>{{cliCreateCommand}} command.", "emptyState": "No tokens found", "deleteToken": { "delete": "Delete Token", diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index d198894dd5e0f..ab7d03a13a64c 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -2,7 +2,7 @@ import { FC, PropsWithChildren, useState } from "react" import { Section } from "components/SettingsLayout/Section" import { TokensPageView } from "./TokensPageView" import makeStyles from "@material-ui/core/styles/makeStyles" -import { useTranslation } from "react-i18next" +import { useTranslation, Trans } from "react-i18next" import { useTokensData, useCheckTokenPermissions } from "./hooks" import { TokensSwitch, ConfirmDeleteDialog } from "./components" @@ -10,11 +10,12 @@ export const TokensPage: FC> = () => { const styles = useStyles() const { t } = useTranslation("tokensPage") + const cliCreateCommand = "coder tokens create" const description = ( - <> - {t("description")}{" "} - coder tokens create command. - + + Tokens are used to authenticate with the Coder API. You can create a token + with the Coder CLI using the {{ cliCreateCommand }} command. + ) const [tokenIdToDelete, setTokenIdToDelete] = useState( @@ -35,7 +36,12 @@ export const TokensPage: FC> = () => { return ( <> -
+
> = () => { } const useStyles = makeStyles((theme) => ({ - code: { - background: theme.palette.divider, - fontSize: 12, - padding: "2px 4px", - color: theme.palette.text.primary, - borderRadius: 2, + section: { + "& code": { + background: theme.palette.divider, + fontSize: 12, + padding: "2px 4px", + color: theme.palette.text.primary, + borderRadius: 2, + }, }, })) From ff2ac685d124f885eeee36daac502550017efd8d Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 23 Feb 2023 17:17:24 +0000 Subject: [PATCH 04/10] using Trans component on DeleteDialog --- site/src/i18n/en/tokensPage.json | 2 +- .../TokensPage/components/ConfirmDeleteDialog.tsx | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/site/src/i18n/en/tokensPage.json b/site/src/i18n/en/tokensPage.json index b071c1b44e979..024edfaea94ca 100644 --- a/site/src/i18n/en/tokensPage.json +++ b/site/src/i18n/en/tokensPage.json @@ -4,7 +4,7 @@ "emptyState": "No tokens found", "deleteToken": { "delete": "Delete Token", - "deleteCaption": "Are you sure you want to delete this token?", + "deleteCaption": "Are you sure you want to delete this token?

<4>{{tokenId}}", "deleteSuccess": "Token has been deleted", "deleteFailure": "Failed to delete token" }, diff --git a/site/src/pages/UserSettingsPage/TokensPage/components/ConfirmDeleteDialog.tsx b/site/src/pages/UserSettingsPage/TokensPage/components/ConfirmDeleteDialog.tsx index b9c6c483bb6b0..4e34aa524399f 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/components/ConfirmDeleteDialog.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/components/ConfirmDeleteDialog.tsx @@ -1,7 +1,6 @@ import { FC } from "react" import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" -import { useTranslation } from "react-i18next" -import { Typography } from "components/Typography/Typography" +import { useTranslation, Trans } from "react-i18next" import { useDeleteToken } from "../hooks" import { displaySuccess, displayError } from "components/GlobalSnackbar/utils" import { getErrorMessage } from "api/errors" @@ -13,13 +12,13 @@ export const ConfirmDeleteDialog: FC<{ }> = ({ queryKey, tokenId, setTokenId }) => { const { t } = useTranslation("tokensPage") - const content = ( - - {t("deleteToken.deleteCaption")} + const description = ( + + Are you sure you want to delete this token?

- {tokenId} -
+ {{ tokenId }} + ) const { mutate: deleteToken, isLoading: isDeleting } = @@ -39,7 +38,7 @@ export const ConfirmDeleteDialog: FC<{ return ( { From ade9548a62471cb32122fcdb3d968d62c6eec238 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 23 Feb 2023 18:33:45 +0000 Subject: [PATCH 05/10] add owner col --- coderd/apikey.go | 20 ++++++++++++-- site/src/i18n/en/tokensPage.json | 3 ++- .../TokensPage/TokensPage.tsx | 1 + .../TokensPage/TokensPageView.tsx | 27 +++++++++++++++---- .../UserSettingsPage/TokensPage/hooks.ts | 2 +- 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/coderd/apikey.go b/coderd/apikey.go index ef0921d037e0c..6832d56f33431 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -167,6 +167,11 @@ func (api *API) apiKey(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, convertAPIKey(key)) } +type ConvertedAPIKey struct { + codersdk.APIKey + Username string `json:"username"` +} + // @Summary Get user tokens // @ID get-user-tokens // @Security CoderSessionToken @@ -216,9 +221,20 @@ func (api *API) tokens(rw http.ResponseWriter, r *http.Request) { return } - var apiKeys []codersdk.APIKey + var apiKeys []ConvertedAPIKey for _, key := range keys { - apiKeys = append(apiKeys, convertAPIKey(key)) + user, err := api.Database.GetUserByID(ctx, key.UserID) + if err != nil { + apiKeys = append(apiKeys, ConvertedAPIKey{ + APIKey: convertAPIKey(key), + Username: "", + }) + } else { + apiKeys = append(apiKeys, ConvertedAPIKey{ + APIKey: convertAPIKey(key), + Username: user.Username, + }) + } } httpapi.Write(ctx, rw, http.StatusOK, apiKeys) diff --git a/site/src/i18n/en/tokensPage.json b/site/src/i18n/en/tokensPage.json index 024edfaea94ca..a6b4cc3f94d7e 100644 --- a/site/src/i18n/en/tokensPage.json +++ b/site/src/i18n/en/tokensPage.json @@ -13,6 +13,7 @@ "id": "ID", "createdAt": "Created At", "lastUsed": "Last Used", - "expiresAt": "Expires At" + "expiresAt": "Expires At", + "owner": "Owner" } } diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index ab7d03a13a64c..b7af557668d7a 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -49,6 +49,7 @@ export const TokensPage: FC> = () => { /> { return now.isBefore(t.add(100, "year")) ? t.fromNow() : "Never" } +interface ConvertedAPIKey extends APIKey { + username: string +} + export interface TokensPageViewProps { - tokens?: APIKey[] + tokens?: ConvertedAPIKey[] + viewAllTokens: boolean getTokensError?: Error | unknown isLoading: boolean hasLoaded: boolean @@ -36,6 +41,7 @@ export const TokensPageView: FC< React.PropsWithChildren > = ({ tokens, + viewAllTokens, getTokensError, isLoading, hasLoaded, @@ -44,6 +50,7 @@ export const TokensPageView: FC< }) => { const theme = useTheme() const { t } = useTranslation("tokensPage") + const colWidth = viewAllTokens ? "20%" : "25%" return ( @@ -57,10 +64,13 @@ export const TokensPageView: FC< - {t("table.id")} - {t("table.createdAt")} - {t("table.lastUsed")} - {t("table.expiresAt")} + {t("table.id")} + {t("table.createdAt")} + {t("table.lastUsed")} + {t("table.expiresAt")} + {viewAllTokens && ( + {t("table.owner")} + )} @@ -102,6 +112,13 @@ export const TokensPageView: FC< {dayjs(token.expires_at).fromNow()} + {viewAllTokens && ( + + + {token.username} + + + )} { const queryKey = ["auth"] const params = { From dd587e86fbafb97fa7302df77f4daa4c72ad82dc Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Thu, 23 Feb 2023 18:42:34 +0000 Subject: [PATCH 06/10] simplify hook return --- site/src/pages/UserSettingsPage/TokensPage/hooks.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/site/src/pages/UserSettingsPage/TokensPage/hooks.ts b/site/src/pages/UserSettingsPage/TokensPage/hooks.ts index 281d4bafaf35d..ec69a8a59ca8c 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/hooks.ts +++ b/site/src/pages/UserSettingsPage/TokensPage/hooks.ts @@ -23,14 +23,10 @@ export const useCheckTokenPermissions = () => { }, }, } - const result = useQuery({ + return useQuery({ queryKey, queryFn: () => checkAuthorization(params), }) - - return { - ...result, - } } // Load all tokens From 2ce518c02327ff4f39b4e0b1df85425f8bd49994 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Fri, 24 Feb 2023 16:22:27 +0000 Subject: [PATCH 07/10] lint --- .vscode/settings.json | 1 + site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx | 7 ++++++- .../pages/UserSettingsPage/TokensPage/TokensPageView.tsx | 6 +----- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 18adff1b26787..d4c34f3a9a106 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "agentsdk", "apps", "ASKPASS", + "authcheck", "autostop", "awsidentity", "bodyclose", diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index b7af557668d7a..9d740db5022b4 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -5,6 +5,11 @@ import makeStyles from "@material-ui/core/styles/makeStyles" import { useTranslation, Trans } from "react-i18next" import { useTokensData, useCheckTokenPermissions } from "./hooks" import { TokensSwitch, ConfirmDeleteDialog } from "./components" +import { APIKey } from "api/typesGenerated" + +export interface ConvertedAPIKey extends APIKey { + username: string +} export const TokensPage: FC> = () => { const styles = useStyles() @@ -48,7 +53,7 @@ export const TokensPage: FC> = () => { setViewAllTokens={setViewAllTokens} /> { const t = dayjs(lastUsed) @@ -23,10 +23,6 @@ const lastUsedOrNever = (lastUsed: string) => { return now.isBefore(t.add(100, "year")) ? t.fromNow() : "Never" } -interface ConvertedAPIKey extends APIKey { - username: string -} - export interface TokensPageViewProps { tokens?: ConvertedAPIKey[] viewAllTokens: boolean From 740c4a2d4ad7371523c71f1123f57cfeb926b2b8 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Fri, 24 Feb 2023 18:31:01 +0000 Subject: [PATCH 08/10] type for response --- cli/tokens.go | 21 ++++--------------- coderd/apikey.go | 11 +++------- codersdk/apikey.go | 9 ++++++-- site/src/api/api.ts | 4 ++-- site/src/api/typesGenerated.ts | 5 +++++ .../TokensPage/TokensPage.tsx | 7 +------ .../TokensPage/TokensPageView.tsx | 2 +- 7 files changed, 23 insertions(+), 36 deletions(-) diff --git a/cli/tokens.go b/cli/tokens.go index 427e45606fcc3..1fcbde1d6a1ee 100644 --- a/cli/tokens.go +++ b/cli/tokens.go @@ -6,7 +6,6 @@ import ( "strings" "time" - "github.com/google/uuid" "github.com/spf13/cobra" "golang.org/x/exp/slices" "golang.org/x/xerrors" @@ -99,16 +98,14 @@ type tokenListRow struct { Owner string `json:"-" table:"owner"` } -func tokenListRowFromToken(token codersdk.APIKey, usersByID map[uuid.UUID]codersdk.User) tokenListRow { - user := usersByID[token.UserID] - +func tokenListRowFromToken(token codersdk.ConvertedAPIKey) tokenListRow { return tokenListRow{ - APIKey: token, + APIKey: token.APIKey, ID: token.ID, LastUsed: token.LastUsed, ExpiresAt: token.ExpiresAt, CreatedAt: token.CreatedAt, - Owner: user.Username, + Owner: token.Username, } } @@ -150,20 +147,10 @@ func listTokens() *cobra.Command { )) } - userRes, err := client.Users(cmd.Context(), codersdk.UsersRequest{}) - if err != nil { - return err - } - - usersByID := map[uuid.UUID]codersdk.User{} - for _, user := range userRes.Users { - usersByID[user.ID] = user - } - displayTokens = make([]tokenListRow, len(tokens)) for i, token := range tokens { - displayTokens[i] = tokenListRowFromToken(token, usersByID) + displayTokens[i] = tokenListRowFromToken(token) } out, err := formatter.Format(cmd.Context(), displayTokens) diff --git a/coderd/apikey.go b/coderd/apikey.go index 6832d56f33431..2ce15ef6135f0 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -167,11 +167,6 @@ func (api *API) apiKey(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, convertAPIKey(key)) } -type ConvertedAPIKey struct { - codersdk.APIKey - Username string `json:"username"` -} - // @Summary Get user tokens // @ID get-user-tokens // @Security CoderSessionToken @@ -221,16 +216,16 @@ func (api *API) tokens(rw http.ResponseWriter, r *http.Request) { return } - var apiKeys []ConvertedAPIKey + var apiKeys []codersdk.ConvertedAPIKey for _, key := range keys { user, err := api.Database.GetUserByID(ctx, key.UserID) if err != nil { - apiKeys = append(apiKeys, ConvertedAPIKey{ + apiKeys = append(apiKeys, codersdk.ConvertedAPIKey{ APIKey: convertAPIKey(key), Username: "", }) } else { - apiKeys = append(apiKeys, ConvertedAPIKey{ + apiKeys = append(apiKeys, codersdk.ConvertedAPIKey{ APIKey: convertAPIKey(key), Username: user.Username, }) diff --git a/codersdk/apikey.go b/codersdk/apikey.go index d84e3debbf406..c7e6bce133706 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -90,6 +90,11 @@ type TokensFilter struct { IncludeAll bool `json:"include_all"` } +type ConvertedAPIKey struct { + APIKey + Username string `json:"username"` +} + // asRequestOption returns a function that can be used in (*Client).Request. // It modifies the request query parameters. func (f TokensFilter) asRequestOption() RequestOption { @@ -101,7 +106,7 @@ func (f TokensFilter) asRequestOption() RequestOption { } // Tokens list machine API keys. -func (c *Client) Tokens(ctx context.Context, userID string, filter TokensFilter) ([]APIKey, error) { +func (c *Client) Tokens(ctx context.Context, userID string, filter TokensFilter) ([]ConvertedAPIKey, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), nil, filter.asRequestOption()) if err != nil { return nil, err @@ -110,7 +115,7 @@ func (c *Client) Tokens(ctx context.Context, userID string, filter TokensFilter) if res.StatusCode > http.StatusOK { return nil, ReadBodyAsError(res) } - apiKey := []APIKey{} + apiKey := []ConvertedAPIKey{} return apiKey, json.NewDecoder(res.Body).Decode(&apiKey) } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 303042f2ccf5e..2b9dbc32cd5a0 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -142,8 +142,8 @@ export const getApiKey = async (): Promise => { export const getTokens = async ( params: TypesGen.TokensFilter, -): Promise => { - const response = await axios.get( +): Promise => { + const response = await axios.get( `/api/v2/users/me/keys/tokens`, { params, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index cf36bd98bff3b..7849875f19b53 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -135,6 +135,11 @@ export interface ComputedParameter extends Parameter { readonly default_source_value: boolean } +// From codersdk/apikey.go +export interface ConvertedAPIKey extends APIKey { + readonly username: string +} + // From codersdk/users.go export interface CreateFirstUserRequest { readonly email: string diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx index 9d740db5022b4..b7af557668d7a 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPage.tsx @@ -5,11 +5,6 @@ import makeStyles from "@material-ui/core/styles/makeStyles" import { useTranslation, Trans } from "react-i18next" import { useTokensData, useCheckTokenPermissions } from "./hooks" import { TokensSwitch, ConfirmDeleteDialog } from "./components" -import { APIKey } from "api/typesGenerated" - -export interface ConvertedAPIKey extends APIKey { - username: string -} export const TokensPage: FC> = () => { const styles = useStyles() @@ -53,7 +48,7 @@ export const TokensPage: FC> = () => { setViewAllTokens={setViewAllTokens} /> { const t = dayjs(lastUsed) From b54c70bb815530833ee6d72acf6244a094112add Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Tue, 28 Feb 2023 20:11:37 +0000 Subject: [PATCH 09/10] PR feedback --- cli/tokens.go | 2 +- coderd/apikey.go | 25 +++++++++++++------ codersdk/apikey.go | 6 ++--- site/src/api/api.ts | 4 +-- site/src/api/typesGenerated.ts | 10 ++++---- .../TokensPage/TokensPageView.tsx | 4 +-- 6 files changed, 31 insertions(+), 20 deletions(-) diff --git a/cli/tokens.go b/cli/tokens.go index 1fcbde1d6a1ee..082d83303824a 100644 --- a/cli/tokens.go +++ b/cli/tokens.go @@ -98,7 +98,7 @@ type tokenListRow struct { Owner string `json:"-" table:"owner"` } -func tokenListRowFromToken(token codersdk.ConvertedAPIKey) tokenListRow { +func tokenListRowFromToken(token codersdk.APIKeyWithOwner) tokenListRow { return tokenListRow{ APIKey: token.APIKey, ID: token.ID, diff --git a/coderd/apikey.go b/coderd/apikey.go index 2ce15ef6135f0..f546a00eda5eb 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -216,18 +216,29 @@ func (api *API) tokens(rw http.ResponseWriter, r *http.Request) { return } - var apiKeys []codersdk.ConvertedAPIKey + var userIds []uuid.UUID for _, key := range keys { - user, err := api.Database.GetUserByID(ctx, key.UserID) - if err != nil { - apiKeys = append(apiKeys, codersdk.ConvertedAPIKey{ + userIds = append(userIds, key.UserID) + } + fmt.Println("userIds", userIds) + + users, _ := api.Database.GetUsersByIDs(ctx, userIds) + usersByID := map[uuid.UUID]database.User{} + for _, user := range users { + usersByID[user.ID] = user + } + + var apiKeys []codersdk.APIKeyWithOwner + for _, key := range keys { + if user, exists := usersByID[key.UserID]; exists { + apiKeys = append(apiKeys, codersdk.APIKeyWithOwner{ APIKey: convertAPIKey(key), - Username: "", + Username: user.Username, }) } else { - apiKeys = append(apiKeys, codersdk.ConvertedAPIKey{ + apiKeys = append(apiKeys, codersdk.APIKeyWithOwner{ APIKey: convertAPIKey(key), - Username: user.Username, + Username: "", }) } } diff --git a/codersdk/apikey.go b/codersdk/apikey.go index c7e6bce133706..ba24211169072 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -90,7 +90,7 @@ type TokensFilter struct { IncludeAll bool `json:"include_all"` } -type ConvertedAPIKey struct { +type APIKeyWithOwner struct { APIKey Username string `json:"username"` } @@ -106,7 +106,7 @@ func (f TokensFilter) asRequestOption() RequestOption { } // Tokens list machine API keys. -func (c *Client) Tokens(ctx context.Context, userID string, filter TokensFilter) ([]ConvertedAPIKey, error) { +func (c *Client) Tokens(ctx context.Context, userID string, filter TokensFilter) ([]APIKeyWithOwner, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys/tokens", userID), nil, filter.asRequestOption()) if err != nil { return nil, err @@ -115,7 +115,7 @@ func (c *Client) Tokens(ctx context.Context, userID string, filter TokensFilter) if res.StatusCode > http.StatusOK { return nil, ReadBodyAsError(res) } - apiKey := []ConvertedAPIKey{} + apiKey := []APIKeyWithOwner{} return apiKey, json.NewDecoder(res.Body).Decode(&apiKey) } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 104573b03d06e..03438161120f5 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -142,8 +142,8 @@ export const getApiKey = async (): Promise => { export const getTokens = async ( params: TypesGen.TokensFilter, -): Promise => { - const response = await axios.get( +): Promise => { + const response = await axios.get( `/api/v2/users/me/keys/tokens`, { params, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index bb7b94b220fdc..cc2d5f95feab3 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -13,6 +13,11 @@ export interface APIKey { readonly lifetime_seconds: number } +// From codersdk/apikey.go +export interface APIKeyWithOwner extends APIKey { + readonly username: string +} + // From codersdk/licenses.go export interface AddLicenseRequest { readonly license: string @@ -136,11 +141,6 @@ export interface ComputedParameter extends Parameter { readonly default_source_value: boolean } -// From codersdk/apikey.go -export interface ConvertedAPIKey extends APIKey { - readonly username: string -} - // From codersdk/users.go export interface CreateFirstUserRequest { readonly email: string diff --git a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx index 132ac9f3332f6..547fecc1133d1 100644 --- a/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx +++ b/site/src/pages/UserSettingsPage/TokensPage/TokensPageView.tsx @@ -15,7 +15,7 @@ import { FC } from "react" import { AlertBanner } from "components/AlertBanner/AlertBanner" import IconButton from "@material-ui/core/IconButton/IconButton" import { useTranslation } from "react-i18next" -import { ConvertedAPIKey } from "api/typesGenerated" +import { APIKeyWithOwner } from "api/typesGenerated" const lastUsedOrNever = (lastUsed: string) => { const t = dayjs(lastUsed) @@ -24,7 +24,7 @@ const lastUsedOrNever = (lastUsed: string) => { } export interface TokensPageViewProps { - tokens?: ConvertedAPIKey[] + tokens?: APIKeyWithOwner[] viewAllTokens: boolean getTokensError?: Error | unknown isLoading: boolean From 7b87fb1055c3bb7e2f0c6c6d68d7ee7a69bc0984 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Wed, 1 Mar 2023 15:57:19 +0000 Subject: [PATCH 10/10] fix lint --- coderd/apikey.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/apikey.go b/coderd/apikey.go index f546a00eda5eb..1bdafe9007d9f 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -220,7 +220,6 @@ func (api *API) tokens(rw http.ResponseWriter, r *http.Request) { for _, key := range keys { userIds = append(userIds, key.UserID) } - fmt.Println("userIds", userIds) users, _ := api.Database.GetUsersByIDs(ctx, userIds) usersByID := map[uuid.UUID]database.User{}