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
       }
     }
   }