diff --git a/apps/dashboard/components/api-keys/list.tsx b/apps/dashboard/components/api-keys/list.tsx index 6f94897c43..2ca7d17b1a 100644 --- a/apps/dashboard/components/api-keys/list.tsx +++ b/apps/dashboard/components/api-keys/list.tsx @@ -4,7 +4,7 @@ import Table from "@mui/joy/Table" import { getServerSession } from "next-auth" import { redirect } from "next/navigation" -import { Typography } from "@mui/joy" +import { Divider, Typography } from "@mui/joy" import RevokeKey from "./revoke" @@ -30,25 +30,30 @@ const ApiKeysList = async () => { const keys = await apiKeys(token) + const activeKeys = keys.filter(({ expired, revoked }) => !expired && !revoked) + const expiredKeys = keys.filter(({ expired }) => expired) + const revokedKeys = keys.filter(({ revoked }) => revoked) + return ( <> + Active Keys - - + + - {keys.map(({ id, name, createdAt, expiresAt }) => ( + {activeKeys.map(({ id, name, expiresAt, lastUsedAt }) => ( - - + + @@ -56,7 +61,57 @@ const ApiKeysList = async () => { ))}
API Key ID NameCreated AtAPI Key ID Expires AtLast Used Action
{id} {name}{formatDate(createdAt)}{id} {formatDate(expiresAt)}{lastUsedAt ? formatDate(lastUsedAt) : "-"}
- {keys.length === 0 && No keys to display.} + {activeKeys.length === 0 && No keys to display.} + + + + Revoked Keys + + + + + + + + + + + {revokedKeys.map(({ id, name, expiresAt, createdAt }) => ( + + + + + + + ))} + +
NameAPI Key IDCreated AtLast Used
{name}{id}{formatDate(createdAt)}{formatDate(expiresAt)}
+ {revokedKeys.length === 0 && No keys to display.} + + + + Expired Keys + + + + + + + + + + + {expiredKeys.map(({ id, name, expiresAt, createdAt }) => ( + + + + + + + ))} + +
NameAPI Key IDCreated AtLast Used
{name}{id}{formatDate(createdAt)}{formatDate(expiresAt)}
+ {expiredKeys.length === 0 && No keys to display.} ) } diff --git a/apps/dashboard/services/graphql/generated.ts b/apps/dashboard/services/graphql/generated.ts index 554d8c3604..d0faccc9fd 100644 --- a/apps/dashboard/services/graphql/generated.ts +++ b/apps/dashboard/services/graphql/generated.ts @@ -203,9 +203,12 @@ export type AccountUpdateNotificationSettingsPayload = { export type ApiKey = { readonly __typename: 'ApiKey'; readonly createdAt: Scalars['DateTime']['output']; + readonly expired: Scalars['Boolean']['output']; readonly expiresAt: Scalars['DateTime']['output']; readonly id: Scalars['ID']['output']; + readonly lastUsedAt?: Maybe; readonly name: Scalars['String']['output']; + readonly revoked: Scalars['Boolean']['output']; }; export type ApiKeyCreateInput = { @@ -1906,7 +1909,7 @@ 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: string, readonly expiresAt: string } } }; +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: string, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: string | null, readonly expiresAt: string } } }; export type ApiKeyRevokeMutationVariables = Exact<{ input: ApiKeyRevokeInput; @@ -1937,7 +1940,7 @@ export type UserEmailDeleteMutation = { readonly __typename: 'Mutation', readonl 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: string, readonly expiresAt: string }> } | 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: string, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: string | null, readonly expiresAt: string }> } | null }; export type GetPaginatedTransactionsQueryVariables = Exact<{ first?: InputMaybe; @@ -1968,6 +1971,9 @@ export const ApiKeyCreateDocument = gql` id name createdAt + revoked + expired + lastUsedAt expiresAt } apiKeySecret @@ -2146,6 +2152,9 @@ export const ApiKeysDocument = gql` id name createdAt + revoked + expired + lastUsedAt expiresAt } } @@ -2964,9 +2973,12 @@ export type AccountUpdateNotificationSettingsPayloadResolvers = { createdAt?: Resolver; + expired?: Resolver; expiresAt?: Resolver; id?: Resolver; + lastUsedAt?: Resolver, ParentType, ContextType>; name?: Resolver; + revoked?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/apps/dashboard/services/graphql/mutations/api-keys.ts b/apps/dashboard/services/graphql/mutations/api-keys.ts index 4244dd6b7e..a823ce6a0d 100644 --- a/apps/dashboard/services/graphql/mutations/api-keys.ts +++ b/apps/dashboard/services/graphql/mutations/api-keys.ts @@ -15,6 +15,9 @@ gql` id name createdAt + revoked + expired + lastUsedAt expiresAt } apiKeySecret diff --git a/apps/dashboard/services/graphql/queries/api-keys.ts b/apps/dashboard/services/graphql/queries/api-keys.ts index d2fbffe317..78ae8bbce7 100644 --- a/apps/dashboard/services/graphql/queries/api-keys.ts +++ b/apps/dashboard/services/graphql/queries/api-keys.ts @@ -10,6 +10,9 @@ gql` id name createdAt + revoked + expired + lastUsedAt expiresAt } } @@ -20,10 +23,10 @@ export async function apiKeys(token: string) { const client = apollo(token).getClient() try { - const data = await client.query({ + const { data } = await client.query({ query: ApiKeysDocument, }) - return data.data.me?.apiKeys || [] + return data.me?.apiKeys || [] } catch (err) { if (err instanceof Error) { console.error("error", err) diff --git a/bats/core/api-keys/api-keys.bats b/bats/core/api-keys/api-keys.bats index 23414afca4..b4119d44d7 100644 --- a/bats/core/api-keys/api-keys.bats +++ b/bats/core/api-keys/api-keys.bats @@ -45,6 +45,8 @@ new_key_name() { name=$(echo "$key" | jq -r '.name') [[ "${name}" = "${key_name}" ]] || exit 1 + key_id=$(echo "$key" | jq -r '.id') + cache_value "api-key-id" "$key_id" } @test "api-keys: can authenticate with api key and list keys" { @@ -53,3 +55,20 @@ new_key_name() { keyName="$(graphql_output '.data.me.apiKeys[-1].name')" [[ "${keyName}" = "$(read_value 'key_name')" ]] || exit 1 } + +@test "api-keys: can revoke key" { + key_id=$(read_value "api-key-id") + variables="{\"input\":{\"id\":\"${key_id}\"}}" + + exec_graphql 'alice' 'revoke-api-key' "$variables" + + exec_graphql 'alice' 'api-keys' + + revoked="$(graphql_output '.data.me.apiKeys[-1].revoked')" + [[ "${revoked}" = "true" ]] || exit 1 + + exec_graphql 'api-key-secret' 'api-keys' + + error="$(graphql_output '.error.code')" + [[ "${error}" = "401" ]] || exit 1 +} diff --git a/bats/gql/api-keys.gql b/bats/gql/api-keys.gql index e44fe81bf9..a436709da5 100644 --- a/bats/gql/api-keys.gql +++ b/bats/gql/api-keys.gql @@ -6,6 +6,7 @@ query apiKeys { apiKeys { id name + revoked createdAt expiresAt } diff --git a/bats/gql/revoke-api-key.gql b/bats/gql/revoke-api-key.gql new file mode 100644 index 0000000000..23cd7dc438 --- /dev/null +++ b/bats/gql/revoke-api-key.gql @@ -0,0 +1,3 @@ +mutation ApiKeyRevoke($input: ApiKeyRevokeInput!) { + apiKeyRevoke(input: $input) +} diff --git a/core/api-keys/.sqlx/query-89243f68fc159de3fd43fef1abe161d5400ecc0392adeda32f135bfbe3272057.json b/core/api-keys/.sqlx/query-55b7329b0b77d904042d36e5e6039043cc02fa498377de03561943c4e4d433de.json similarity index 54% rename from core/api-keys/.sqlx/query-89243f68fc159de3fd43fef1abe161d5400ecc0392adeda32f135bfbe3272057.json rename to core/api-keys/.sqlx/query-55b7329b0b77d904042d36e5e6039043cc02fa498377de03561943c4e4d433de.json index a37df307f2..f7578a6a1d 100644 --- a/core/api-keys/.sqlx/query-89243f68fc159de3fd43fef1abe161d5400ecc0392adeda32f135bfbe3272057.json +++ b/core/api-keys/.sqlx/query-55b7329b0b77d904042d36e5e6039043cc02fa498377de03561943c4e4d433de.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n i.id AS identity_id,\n a.id AS api_key_id,\n a.name,\n a.created_at,\n a.expires_at\n FROM\n identities i\n JOIN\n identity_api_keys a\n ON i.id = a.identity_id\n WHERE\n i.subject_id = $1\n AND a.active = true\n AND a.expires_at > NOW() AT TIME ZONE 'utc'\n ", + "query": "\n SELECT\n i.id AS identity_id,\n a.id AS api_key_id,\n a.name,\n a.created_at,\n a.expires_at,\n revoked,\n expires_at < NOW() AS \"expired!\",\n last_used_at\n FROM\n identities i\n JOIN\n identity_api_keys a\n ON i.id = a.identity_id\n WHERE\n i.subject_id = $1\n ", "describe": { "columns": [ { @@ -27,6 +27,21 @@ "ordinal": 4, "name": "expires_at", "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "revoked", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "expired!", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "last_used_at", + "type_info": "Timestamptz" } ], "parameters": { @@ -39,8 +54,11 @@ false, false, false, - false + false, + false, + null, + true ] }, - "hash": "89243f68fc159de3fd43fef1abe161d5400ecc0392adeda32f135bfbe3272057" + "hash": "55b7329b0b77d904042d36e5e6039043cc02fa498377de03561943c4e4d433de" } diff --git a/core/api-keys/.sqlx/query-6ed03c625536a19f2fa96fe5652c56140df4fd4370319c229ed52e8f07868f7e.json b/core/api-keys/.sqlx/query-6ed03c625536a19f2fa96fe5652c56140df4fd4370319c229ed52e8f07868f7e.json deleted file mode 100644 index da1565ec5d..0000000000 --- a/core/api-keys/.sqlx/query-6ed03c625536a19f2fa96fe5652c56140df4fd4370319c229ed52e8f07868f7e.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT i.id, i.subject_id\n FROM identities i\n JOIN identity_api_keys k ON k.identity_id = i.id\n WHERE k.active = true AND k.encrypted_key = crypt($1, encrypted_key)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "subject_id", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false - ] - }, - "hash": "6ed03c625536a19f2fa96fe5652c56140df4fd4370319c229ed52e8f07868f7e" -} diff --git a/core/api-keys/.sqlx/query-f24b1db7d39e22c52fdde28beb1936f27d49ab435330d44cc4bdfb601e1e1df5.json b/core/api-keys/.sqlx/query-f24b1db7d39e22c52fdde28beb1936f27d49ab435330d44cc4bdfb601e1e1df5.json new file mode 100644 index 0000000000..984441e2bb --- /dev/null +++ b/core/api-keys/.sqlx/query-f24b1db7d39e22c52fdde28beb1936f27d49ab435330d44cc4bdfb601e1e1df5.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE identity_api_keys k\n SET revoked = true,\n revoked_at = NOW()\n FROM identities i\n WHERE k.identity_id = i.id\n AND i.subject_id = $1\n AND k.id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "f24b1db7d39e22c52fdde28beb1936f27d49ab435330d44cc4bdfb601e1e1df5" +} diff --git a/core/api-keys/.sqlx/query-f30599b0dc2363e69649142ae3e0f8768cdd94f2b04334b9b5b548b8cb113a33.json b/core/api-keys/.sqlx/query-f30599b0dc2363e69649142ae3e0f8768cdd94f2b04334b9b5b548b8cb113a33.json new file mode 100644 index 0000000000..77bb666dd0 --- /dev/null +++ b/core/api-keys/.sqlx/query-f30599b0dc2363e69649142ae3e0f8768cdd94f2b04334b9b5b548b8cb113a33.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "WITH updated_key AS (\n UPDATE identity_api_keys k\n SET last_used_at = NOW()\n FROM identities i\n WHERE k.identity_id = i.id\n AND k.revoked = false\n AND k.encrypted_key = crypt($1, k.encrypted_key)\n AND k.expires_at > NOW()\n RETURNING k.id, i.subject_id\n )\n SELECT subject_id FROM updated_key", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "subject_id", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "f30599b0dc2363e69649142ae3e0f8768cdd94f2b04334b9b5b548b8cb113a33" +} diff --git a/core/api-keys/migrations/20231103084227_api_keys_setup.sql b/core/api-keys/migrations/20231103084227_api_keys_setup.sql index e4a4b9d01f..db72dffd40 100644 --- a/core/api-keys/migrations/20231103084227_api_keys_setup.sql +++ b/core/api-keys/migrations/20231103084227_api_keys_setup.sql @@ -10,9 +10,10 @@ CREATE TABLE identity_api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), identity_id UUID REFERENCES identities(id) NOT NULL, name VARCHAR NOT NULL, + last_used_at TIMESTAMPTZ, encrypted_key VARCHAR NOT NULL, - active BOOL NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL, + revoked BOOL NOT NULL DEFAULT false, revoked_at TIMESTAMPTZ ); diff --git a/core/api-keys/src/app/mod.rs b/core/api-keys/src/app/mod.rs index 1ed9e017b4..76af1f4112 100644 --- a/core/api-keys/src/app/mod.rs +++ b/core/api-keys/src/app/mod.rs @@ -59,4 +59,13 @@ impl ApiKeysApp { ) -> Result, ApplicationError> { Ok(self.identities.list_keys_for_subject(subject_id).await?) } + + pub async fn revoke_api_key_for_subject( + &self, + subject: &str, + key_id: IdentityApiKeyId, + ) -> Result<(), ApplicationError> { + self.identities.revoke_api_key(subject, key_id).await?; + Ok(()) + } } diff --git a/core/api-keys/src/graphql/convert.rs b/core/api-keys/src/graphql/convert.rs index f8ddfb24a6..ec11eb7d7d 100644 --- a/core/api-keys/src/graphql/convert.rs +++ b/core/api-keys/src/graphql/convert.rs @@ -10,15 +10,23 @@ use crate::{ use super::schema::{ApiKey, ApiKeyCreatePayload}; +impl From for ApiKey { + fn from(key: IdentityApiKey) -> Self { + ApiKey { + id: key.id.to_string().into(), + name: key.name, + revoked: key.revoked, + expired: key.expired, + last_used_at: key.last_used_at, + created_at: key.created_at, + expires_at: key.expires_at, + } + } +} impl From<(IdentityApiKey, ApiKeySecret)> for ApiKeyCreatePayload { fn from((key, secret): (IdentityApiKey, ApiKeySecret)) -> Self { Self { - api_key: ApiKey { - id: key.id.to_string().into(), - name: key.name, - created_at: key.created_at, - expires_at: key.expires_at, - }, + api_key: ApiKey::from(key), api_key_secret: secret.into_inner(), } } diff --git a/core/api-keys/src/graphql/schema.rs b/core/api-keys/src/graphql/schema.rs index 1d148ea21d..dcb3f07c45 100644 --- a/core/api-keys/src/graphql/schema.rs +++ b/core/api-keys/src/graphql/schema.rs @@ -1,7 +1,7 @@ use async_graphql::*; use chrono::{DateTime, Utc}; -use crate::app::ApiKeysApp; +use crate::{app::ApiKeysApp, identity::IdentityApiKeyId}; pub struct AuthSubject { pub id: String, @@ -22,6 +22,9 @@ pub(super) struct ApiKey { pub id: ID, pub name: String, pub created_at: DateTime, + pub revoked: bool, + pub expired: bool, + pub last_used_at: Option>, pub expires_at: DateTime, } @@ -40,16 +43,7 @@ impl User { let subject = ctx.data::()?; let identity_api_keys = app.list_api_keys_for_subject(&subject.id).await?; - - let api_keys = identity_api_keys - .into_iter() - .map(|identity_key| ApiKey { - id: ID::from(identity_key.id.to_string()), - name: identity_key.name, - created_at: identity_key.created_at, - expires_at: identity_key.expires_at, - }) - .collect(); + let api_keys = identity_api_keys.into_iter().map(ApiKey::from).collect(); Ok(api_keys) } @@ -66,6 +60,7 @@ pub struct Mutation; #[derive(InputObject)] struct ApiKeyCreateInput { name: String, + expire_in_days: Option, } #[derive(InputObject)] @@ -90,9 +85,14 @@ impl Mutation { async fn api_key_revoke( &self, - _ctx: &Context<'_>, - _input: ApiKeyRevokeInput, + ctx: &Context<'_>, + input: ApiKeyRevokeInput, ) -> async_graphql::Result { + let app = ctx.data_unchecked::(); + let api_key_id = input.id.parse::()?; + let subject = ctx.data::()?; + app.revoke_api_key_for_subject(&subject.id, api_key_id) + .await?; Ok(true) } } diff --git a/core/api-keys/src/identity/error.rs b/core/api-keys/src/identity/error.rs index 1652f4c017..dd0e4b5841 100644 --- a/core/api-keys/src/identity/error.rs +++ b/core/api-keys/src/identity/error.rs @@ -2,6 +2,8 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum IdentityError { + #[error("IdentityError - KeyNotFoundForRevoke")] + KeyNotFoundForRevoke, #[error("IdentityError - MismatchedPrefix")] MismatchedPrefix, #[error("IdentityError - NoActiveKeyFound")] diff --git a/core/api-keys/src/identity/mod.rs b/core/api-keys/src/identity/mod.rs index da880722d8..335b6a66c4 100644 --- a/core/api-keys/src/identity/mod.rs +++ b/core/api-keys/src/identity/mod.rs @@ -10,12 +10,16 @@ pub use error::*; crate::entity_id! { IdentityApiKeyId } crate::entity_id! { IdentityId } +#[derive(Debug)] pub struct IdentityApiKey { pub name: String, pub id: IdentityApiKeyId, pub identity_id: IdentityId, pub created_at: chrono::DateTime, pub expires_at: chrono::DateTime, + pub last_used_at: Option>, + pub revoked: bool, + pub expired: bool, } pub struct ApiKeySecret(String); @@ -79,6 +83,9 @@ impl Identities { identity_id, created_at: record.created_at, expires_at, + revoked: false, + expired: false, + last_used_at: None, }, ApiKeySecret(key), )) @@ -89,11 +96,19 @@ impl Identities { None => return Err(IdentityError::MismatchedPrefix), Some(code) => code, }; + let record = sqlx::query!( - r#"SELECT i.id, i.subject_id - FROM identities i - JOIN identity_api_keys k ON k.identity_id = i.id - WHERE k.active = true AND k.encrypted_key = crypt($1, encrypted_key)"#, + r#"WITH updated_key AS ( + UPDATE identity_api_keys k + SET last_used_at = NOW() + FROM identities i + WHERE k.identity_id = i.id + AND k.revoked = false + AND k.encrypted_key = crypt($1, k.encrypted_key) + AND k.expires_at > NOW() + RETURNING k.id, i.subject_id + ) + SELECT subject_id FROM updated_key"#, code ) .fetch_optional(&self.pool) @@ -117,7 +132,10 @@ impl Identities { a.id AS api_key_id, a.name, a.created_at, - a.expires_at + a.expires_at, + revoked, + expires_at < NOW() AS "expired!", + last_used_at FROM identities i JOIN @@ -125,8 +143,6 @@ impl Identities { ON i.id = a.identity_id WHERE i.subject_id = $1 - AND a.active = true - AND a.expires_at > NOW() AT TIME ZONE 'utc' "#, subject_id, ) @@ -141,9 +157,37 @@ impl Identities { identity_id: IdentityId::from(record.identity_id), created_at: record.created_at, expires_at: record.expires_at, + revoked: record.revoked, + expired: record.expired, + last_used_at: record.last_used_at, }) .collect(); Ok(api_keys) } + + pub async fn revoke_api_key( + &self, + subject_id: &str, + key_id: IdentityApiKeyId, + ) -> Result<(), IdentityError> { + let result = sqlx::query!( + r#"UPDATE identity_api_keys k + SET revoked = true, + revoked_at = NOW() + FROM identities i + WHERE k.identity_id = i.id + AND i.subject_id = $1 + AND k.id = $2"#, + subject_id, + key_id as IdentityApiKeyId + ) + .execute(&self.pool) + .await?; + let num_deleted = result.rows_affected(); + if num_deleted == 0 { + return Err(IdentityError::KeyNotFoundForRevoke); + } + Ok(()) + } } diff --git a/core/api-keys/subgraph/schema.graphql b/core/api-keys/subgraph/schema.graphql index 40f811dd45..b55aa23cf4 100644 --- a/core/api-keys/subgraph/schema.graphql +++ b/core/api-keys/subgraph/schema.graphql @@ -2,11 +2,15 @@ type ApiKey { id: ID! name: String! createdAt: DateTime! + revoked: Boolean! + expired: Boolean! + lastUsedAt: DateTime expiresAt: DateTime! } input ApiKeyCreateInput { name: String! + expireInDays: Int } type ApiKeyCreatePayload { diff --git a/core/api/dev/apollo-federation/supergraph.graphql b/core/api/dev/apollo-federation/supergraph.graphql index 2df7ff39c4..7a459a2bf8 100644 --- a/core/api/dev/apollo-federation/supergraph.graphql +++ b/core/api/dev/apollo-federation/supergraph.graphql @@ -161,6 +161,9 @@ type ApiKey id: ID! name: String! createdAt: DateTime! + revoked: Boolean! + expired: Boolean! + lastUsedAt: DateTime expiresAt: DateTime! } @@ -168,6 +171,7 @@ input ApiKeyCreateInput @join__type(graph: API_KEYS) { name: String! + expireInDays: Int } type ApiKeyCreatePayload diff --git a/dev/config/apollo-federation/supergraph.graphql b/dev/config/apollo-federation/supergraph.graphql index 2df7ff39c4..7a459a2bf8 100644 --- a/dev/config/apollo-federation/supergraph.graphql +++ b/dev/config/apollo-federation/supergraph.graphql @@ -161,6 +161,9 @@ type ApiKey id: ID! name: String! createdAt: DateTime! + revoked: Boolean! + expired: Boolean! + lastUsedAt: DateTime expiresAt: DateTime! } @@ -168,6 +171,7 @@ input ApiKeyCreateInput @join__type(graph: API_KEYS) { name: String! + expireInDays: Int } type ApiKeyCreatePayload