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
- API Key ID |
Name |
- Created At |
+ API Key ID |
Expires At |
+ Last Used |
Action |
- {keys.map(({ id, name, createdAt, expiresAt }) => (
+ {activeKeys.map(({ id, name, expiresAt, lastUsedAt }) => (
- {id} |
{name} |
- {formatDate(createdAt)} |
+ {id} |
{formatDate(expiresAt)} |
+ {lastUsedAt ? formatDate(lastUsedAt) : "-"} |
|
@@ -56,7 +61,57 @@ const ApiKeysList = async () => {
))}
- {keys.length === 0 && No keys to display.}
+ {activeKeys.length === 0 && No keys to display.}
+
+
+
+ Revoked Keys
+
+
+
+ Name |
+ API Key ID |
+ Created At |
+ Last Used |
+
+
+
+ {revokedKeys.map(({ id, name, expiresAt, createdAt }) => (
+
+ {name} |
+ {id} |
+ {formatDate(createdAt)} |
+ {formatDate(expiresAt)} |
+
+ ))}
+
+
+ {revokedKeys.length === 0 && No keys to display.}
+
+
+
+ Expired Keys
+
+
+
+ Name |
+ API Key ID |
+ Created At |
+ Last Used |
+
+
+
+ {expiredKeys.map(({ id, name, expiresAt, createdAt }) => (
+
+ {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