From b477a8c7ea3f84590ccb0859f3cc0ab7dbd64498 Mon Sep 17 00:00:00 2001 From: Siddharth Tiwari <siddharthtiwarikreplind@gmail.com> Date: Thu, 22 Feb 2024 17:06:05 +0530 Subject: [PATCH] feat(dashboard): made changes in dashboard for scopes --- apps/dashboard/app/api-keys/server-actions.ts | 17 ++++++-- .../components/api-keys/api-card.tsx | 3 +- apps/dashboard/components/api-keys/create.tsx | 41 ++++++++++++++----- apps/dashboard/components/api-keys/list.tsx | 13 +++--- apps/dashboard/components/api-keys/utils.ts | 8 +++- .../cypress/e2e/api-keys/api-keys.cy.ts | 5 +++ apps/dashboard/services/graphql/generated.ts | 21 ++++++++-- .../services/graphql/mutations/api-keys.ts | 6 ++- .../services/graphql/queries/api-keys.ts | 1 + 9 files changed, 86 insertions(+), 29 deletions(-) diff --git a/apps/dashboard/app/api-keys/server-actions.ts b/apps/dashboard/app/api-keys/server-actions.ts index 116fa2cdd9..6057977190 100644 --- a/apps/dashboard/app/api-keys/server-actions.ts +++ b/apps/dashboard/app/api-keys/server-actions.ts @@ -53,9 +53,20 @@ export const createApiKeyServerAction = async ( ): Promise<ApiKeyResponse> => { let apiKeyExpiresInDays: number | null = null const apiKeyName = form.get("apiKeyName") - const readOnly = form.get("apiScope") === "readOnly" - const apiKeyExpiresInDaysSelect = form.get("apiKeyExpiresInDaysSelect") + const scopes = [] + if (form.get("readScope")) scopes.push("READ") + if (form.get("receiveScope")) scopes.push("RECEIVE") + if (form.get("writeScope")) scopes.push("WRITE") + + if (scopes.length === 0) { + return { + error: true, + message: "At least one scope is required", + responsePayload: null, + } + } + const apiKeyExpiresInDaysSelect = form.get("apiKeyExpiresInDaysSelect") if (!apiKeyName || typeof apiKeyName !== "string") { return { error: true, @@ -85,7 +96,7 @@ export const createApiKeyServerAction = async ( let data try { - data = await createApiKey(token, apiKeyName, apiKeyExpiresInDays, readOnly) + data = await createApiKey(token, apiKeyName, apiKeyExpiresInDays, scopes) } catch (err) { console.log("error in createApiKey ", err) return { diff --git a/apps/dashboard/components/api-keys/api-card.tsx b/apps/dashboard/components/api-keys/api-card.tsx index 8a9d4d8870..5ce552c96c 100644 --- a/apps/dashboard/components/api-keys/api-card.tsx +++ b/apps/dashboard/components/api-keys/api-card.tsx @@ -15,6 +15,7 @@ interface ApiKey { readonly lastUsedAt?: number | null readonly expiresAt?: number | null readonly readOnly: boolean + readonly scopes: string[] } interface ApiKeysCardProps { @@ -53,7 +54,7 @@ const ApiKeysCard: React.FC<ApiKeysCardProps> = ({ </Box> <Box sx={{ display: "flex", justifyContent: "space-between" }}> <Typography fontSize={13}>Scope</Typography> - <Typography fontSize={13}>{getScopeText(key.readOnly)}</Typography> + <Typography fontSize={13}>{getScopeText(key.scopes)}</Typography> </Box> {!key.revoked && !key.expired && <RevokeKey id={key.id} />} </Card> diff --git a/apps/dashboard/components/api-keys/create.tsx b/apps/dashboard/components/api-keys/create.tsx index 1e0122a885..bc44d217f9 100644 --- a/apps/dashboard/components/api-keys/create.tsx +++ b/apps/dashboard/components/api-keys/create.tsx @@ -13,8 +13,7 @@ import { Tooltip, Select, Option, - Radio, - RadioGroup, + Checkbox, } from "@mui/joy" import InfoOutlined from "@mui/icons-material/InfoOutlined" @@ -301,15 +300,35 @@ const ApiKeyCreate = ({ defaultWalletId }: Prop) => { </FormHelperText> ) : null} <Box> - <Typography>Scope</Typography> - <RadioGroup defaultValue="readAndWrite" name="apiScope"> - <Radio value="readAndWrite" label="Read and Write" /> - <FormHelperText> - Full access: read and write account details. - </FormHelperText> - <Radio value="readOnly" label="Read Only" /> - <FormHelperText>Limited access: view data only.</FormHelperText> - </RadioGroup> + <Typography>Scopes</Typography> + <Box + sx={{ + display: "flex", + flexDirection: "column", + gap: "0.8em", + marginTop: "1em", + }} + > + <Checkbox + data-testid="read-scope-checkbox" + name="readScope" + id="readScope" + label="Read" + value="READ" + /> + <Checkbox + name="receiveScope" + id="receiveScope" + label="Receive" + value="RECEIVE" + /> + <Checkbox + name="writeScope" + id="writeScope" + label="Write" + value="WRITE" + /> + </Box> </Box> <Box sx={{ diff --git a/apps/dashboard/components/api-keys/list.tsx b/apps/dashboard/components/api-keys/list.tsx index 3a7395e2df..cf4b43eb87 100644 --- a/apps/dashboard/components/api-keys/list.tsx +++ b/apps/dashboard/components/api-keys/list.tsx @@ -16,6 +16,7 @@ interface ApiKey { readonly lastUsedAt?: number | null readonly expiresAt?: number | null readonly readOnly: boolean + readonly scopes: string[] } interface ApiKeysListProps { @@ -44,12 +45,12 @@ const ApiKeysList: React.FC<ApiKeysListProps> = ({ </tr> </thead> <tbody> - {activeKeys.map(({ id, name, expiresAt, lastUsedAt, readOnly }) => { + {activeKeys.map(({ id, name, expiresAt, lastUsedAt, scopes }) => { return ( <tr key={id}> <td>{name}</td> <td>{id}</td> - <td>{getScopeText(readOnly)}</td> + <td>{getScopeText(scopes)}</td> <td>{expiresAt ? formatDate(expiresAt) : "Never"}</td> <td>{lastUsedAt ? formatDate(lastUsedAt) : "Never"}</td> <td style={{ textAlign: "right" }}> @@ -76,11 +77,11 @@ const ApiKeysList: React.FC<ApiKeysListProps> = ({ </tr> </thead> <tbody> - {revokedKeys.map(({ id, name, createdAt, readOnly }) => ( + {revokedKeys.map(({ id, name, createdAt, scopes }) => ( <tr key={id}> <td>{name}</td> <td>{id}</td> - <td>{getScopeText(readOnly)}</td> + <td>{getScopeText(scopes)}</td> <td>{formatDate(createdAt)}</td> <td style={{ textAlign: "right" }}>Revoked</td> </tr> @@ -104,11 +105,11 @@ const ApiKeysList: React.FC<ApiKeysListProps> = ({ </tr> </thead> <tbody> - {expiredKeys.map(({ id, name, createdAt, expiresAt, readOnly }) => ( + {expiredKeys.map(({ id, name, createdAt, expiresAt, scopes }) => ( <tr key={id}> <td>{name}</td> <td>{id}</td> - <td>{getScopeText(readOnly)}</td> + <td>{getScopeText(scopes)}</td> <td>{formatDate(createdAt)}</td> <td style={{ textAlign: "right" }}> {expiresAt ? formatDate(expiresAt) : "Never"} diff --git a/apps/dashboard/components/api-keys/utils.ts b/apps/dashboard/components/api-keys/utils.ts index e8890fd876..71b74c2246 100644 --- a/apps/dashboard/components/api-keys/utils.ts +++ b/apps/dashboard/components/api-keys/utils.ts @@ -7,6 +7,10 @@ export const formatDate = (timestamp: number): string => { return new Date(timestamp * 1000).toLocaleDateString(undefined, options) } -export const getScopeText = (readOnly: boolean): string => { - return readOnly ? "Read Only" : "Read and Write" +export const getScopeText = (scopes: string[]): string => { + if (scopes.length > 0) { + return scopes.join(", ") + } else { + return "No Scopes Defined" + } } diff --git a/apps/dashboard/cypress/e2e/api-keys/api-keys.cy.ts b/apps/dashboard/cypress/e2e/api-keys/api-keys.cy.ts index 78e1dd4bfb..416da9fd21 100644 --- a/apps/dashboard/cypress/e2e/api-keys/api-keys.cy.ts +++ b/apps/dashboard/cypress/e2e/api-keys/api-keys.cy.ts @@ -34,6 +34,11 @@ describe("Callback Test", () => { cy.get('[data-testid="create-api-expire-30-days-select"]').should("not.be.disabled") cy.get('[data-testid="create-api-expire-30-days-select"]').click() + cy.get('[data-testid="read-scope-checkbox"]').should("exist") + cy.get('[data-testid="read-scope-checkbox"]').should("be.visible") + cy.get('[data-testid="read-scope-checkbox"]').should("not.be.disabled") + cy.get('[data-testid="read-scope-checkbox"]').click() + cy.get('[data-testid="create-api-create-btn"]').should("exist") cy.get('[data-testid="create-api-create-btn"]').should("be.visible") cy.get('[data-testid="create-api-create-btn"]').should("not.be.disabled") diff --git a/apps/dashboard/services/graphql/generated.ts b/apps/dashboard/services/graphql/generated.ts index 1544317738..db63aa6590 100644 --- a/apps/dashboard/services/graphql/generated.ts +++ b/apps/dashboard/services/graphql/generated.ts @@ -224,12 +224,13 @@ export type ApiKey = { readonly name: Scalars['String']['output']; readonly readOnly: Scalars['Boolean']['output']; readonly revoked: Scalars['Boolean']['output']; + readonly scopes: ReadonlyArray<Scope>; }; export type ApiKeyCreateInput = { readonly expireInDays?: InputMaybe<Scalars['Int']['input']>; readonly name: Scalars['String']['input']; - readonly readOnly?: Scalars['Boolean']['input']; + readonly scopes?: ReadonlyArray<Scope>; }; export type ApiKeyCreatePayload = { @@ -1671,6 +1672,13 @@ export type SatAmountPayload = { readonly errors: ReadonlyArray<Error>; }; +export const Scope = { + Read: 'READ', + Receive: 'RECEIVE', + Write: 'WRITE' +} as const; + +export type Scope = typeof Scope[keyof typeof Scope]; export type SettlementVia = SettlementViaIntraLedger | SettlementViaLn | SettlementViaOnChain; export type SettlementViaIntraLedger = { @@ -2253,14 +2261,14 @@ export type ApiKeyCreateMutationVariables = Exact<{ }>; -export type ApiKeyCreateMutation = { readonly __typename: 'Mutation', readonly apiKeyCreate: { readonly __typename: 'ApiKeyCreatePayload', readonly apiKeySecret: string, readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt?: number | null } } }; +export type ApiKeyCreateMutation = { readonly __typename: 'Mutation', readonly apiKeyCreate: { readonly __typename: 'ApiKeyCreatePayload', readonly apiKeySecret: string, readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt?: number | null, readonly scopes: ReadonlyArray<Scope> } } }; export type ApiKeyRevokeMutationVariables = Exact<{ input: ApiKeyRevokeInput; }>; -export type ApiKeyRevokeMutation = { readonly __typename: 'Mutation', readonly apiKeyRevoke: { readonly __typename: 'ApiKeyRevokePayload', readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt?: number | null } } }; +export type ApiKeyRevokeMutation = { readonly __typename: 'Mutation', readonly apiKeyRevoke: { readonly __typename: 'ApiKeyRevokePayload', readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt?: number | null, readonly scopes: ReadonlyArray<Scope> } } }; export type CallbackEndpointAddMutationVariables = Exact<{ input: CallbackEndpointAddInput; @@ -2329,7 +2337,7 @@ export type UserTotpRegistrationValidateMutation = { readonly __typename: 'Mutat export type ApiKeysQueryVariables = Exact<{ [key: string]: never; }>; -export type ApiKeysQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly apiKeys: ReadonlyArray<{ readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt?: number | null, readonly readOnly: boolean }> } | null }; +export type ApiKeysQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly apiKeys: ReadonlyArray<{ readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: number, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: number | null, readonly expiresAt?: number | null, readonly readOnly: boolean, readonly scopes: ReadonlyArray<Scope> }> } | null }; export type CallbackEndpointsQueryVariables = Exact<{ [key: string]: never; }>; @@ -2383,6 +2391,7 @@ export const ApiKeyCreateDocument = gql` expired lastUsedAt expiresAt + scopes } apiKeySecret } @@ -2425,6 +2434,7 @@ export const ApiKeyRevokeDocument = gql` expired lastUsedAt expiresAt + scopes } } } @@ -2837,6 +2847,7 @@ export const ApiKeysDocument = gql` lastUsedAt expiresAt readOnly + scopes } } } @@ -3511,6 +3522,7 @@ export type ResolversTypes = { SafeInt: ResolverTypeWrapper<Scalars['SafeInt']['output']>; SatAmount: ResolverTypeWrapper<Scalars['SatAmount']['output']>; SatAmountPayload: ResolverTypeWrapper<SatAmountPayload>; + Scope: Scope; Seconds: ResolverTypeWrapper<Scalars['Seconds']['output']>; SettlementVia: ResolverTypeWrapper<ResolversUnionTypes<ResolversTypes>['SettlementVia']>; SettlementViaIntraLedger: ResolverTypeWrapper<SettlementViaIntraLedger>; @@ -3908,6 +3920,7 @@ export type ApiKeyResolvers<ContextType = any, ParentType extends ResolversParen name?: Resolver<ResolversTypes['String'], ParentType, ContextType>; readOnly?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>; revoked?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>; + scopes?: Resolver<ReadonlyArray<ResolversTypes['Scope']>, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>; }; diff --git a/apps/dashboard/services/graphql/mutations/api-keys.ts b/apps/dashboard/services/graphql/mutations/api-keys.ts index e2d5d64f06..14fb0f2985 100644 --- a/apps/dashboard/services/graphql/mutations/api-keys.ts +++ b/apps/dashboard/services/graphql/mutations/api-keys.ts @@ -19,6 +19,7 @@ gql` expired lastUsedAt expiresAt + scopes } apiKeySecret } @@ -34,6 +35,7 @@ gql` expired lastUsedAt expiresAt + scopes } } } @@ -43,13 +45,13 @@ export async function createApiKey( token: string, name: string, expireInDays: number | null, - readOnly: boolean, + scopes: string[], ) { const client = apollo(token).getClient() try { const { data } = await client.mutate<ApiKeyCreateMutation>({ mutation: ApiKeyCreateDocument, - variables: { input: { name, expireInDays, readOnly } }, + variables: { input: { name, expireInDays, scopes } }, }) return data } catch (error) { diff --git a/apps/dashboard/services/graphql/queries/api-keys.ts b/apps/dashboard/services/graphql/queries/api-keys.ts index ddbc99e6f9..f8a489e82c 100644 --- a/apps/dashboard/services/graphql/queries/api-keys.ts +++ b/apps/dashboard/services/graphql/queries/api-keys.ts @@ -15,6 +15,7 @@ gql` lastUsedAt expiresAt readOnly + scopes } } }