diff --git a/client/src/app/forms/components/form-errors-alert/form-errors-alert.component.ts b/client/src/app/forms/components/form-errors-alert/form-errors-alert.component.ts
index 8c6596a46..ae8beacc1 100644
--- a/client/src/app/forms/components/form-errors-alert/form-errors-alert.component.ts
+++ b/client/src/app/forms/components/form-errors-alert/form-errors-alert.component.ts
@@ -4,6 +4,7 @@ import { Component, Input, OnInit } from '@angular/core'
selector: 'cvc-form-errors-alert',
templateUrl: './form-errors-alert.component.html',
styleUrls: ['./form-errors-alert.component.less'],
+ standalone: false,
})
export class CvcFormErrorsAlertComponent implements OnInit {
@Input() errors!: any
diff --git a/client/src/app/forms/components/user-api-keys/user-api-keys.form.html b/client/src/app/forms/components/user-api-keys/user-api-keys.form.html
new file mode 100644
index 000000000..59f0eb5b2
--- /dev/null
+++ b/client/src/app/forms/components/user-api-keys/user-api-keys.form.html
@@ -0,0 +1,85 @@
+
+
+
+
+ 0">
+
+
+
+
+ @if(newApiKey(); as key) {
+
+
+ -
+ {{ key.token }}
+ {{ key.createdAt | date }}
+
+
+
+
+ }
+
+
+ @for(key of apiKeys(); track key) {
+ -
+ {{ key.reminder }}
+ {{ key.createdAt | date }}
+
+
+ }
+
+
+
+
+
diff --git a/client/src/app/forms/components/user-api-keys/user-api-keys.form.less b/client/src/app/forms/components/user-api-keys/user-api-keys.form.less
new file mode 100644
index 000000000..2964dd350
--- /dev/null
+++ b/client/src/app/forms/components/user-api-keys/user-api-keys.form.less
@@ -0,0 +1,11 @@
+:host {
+ display: block;
+}
+
+nz-space {
+ width: 100%;
+}
+
+nz-form-item:last-of-type {
+ margin-bottom: 0;
+}
diff --git a/client/src/app/forms/components/user-api-keys/user-api-keys.form.ts b/client/src/app/forms/components/user-api-keys/user-api-keys.form.ts
new file mode 100644
index 000000000..b78d33268
--- /dev/null
+++ b/client/src/app/forms/components/user-api-keys/user-api-keys.form.ts
@@ -0,0 +1,171 @@
+import { Component, OnDestroy, signal, WritableSignal } from '@angular/core'
+import { NetworkErrorsService } from '@app/core/services/network-errors.service'
+import {
+ MutationState,
+ MutatorWithState,
+} from '@app/core/utilities/mutation-state-wrapper'
+import {
+ ApiKey,
+ ApiKeysGQL,
+ GenerateApiKeyGQL,
+ GenerateApiKeyInput,
+ GenerateApiKeyMutation,
+ GenerateApiKeyMutationVariables,
+ Maybe,
+ RevokeApiKeyGQL,
+ RevokeApiKeyInput,
+ RevokeApiKeyMutation,
+ RevokeApiKeyMutationVariables,
+} from '@app/generated/civic.apollo'
+import { CommonModule } from '@angular/common'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { NzCardModule } from 'ng-zorro-antd/card'
+import { NzAlertModule } from 'ng-zorro-antd/alert'
+import { NzFormModule } from 'ng-zorro-antd/form'
+import { CvcFormErrorsAlertModule } from '@app/forms/components/form-errors-alert/form-errors-alert.module'
+import { NzRadioModule } from 'ng-zorro-antd/radio'
+import { NzSpinModule } from 'ng-zorro-antd/spin'
+import { NzButtonModule } from 'ng-zorro-antd/button'
+
+import { Subject } from 'rxjs'
+import { filter, takeUntil } from 'rxjs/operators'
+import { NzListModule } from 'ng-zorro-antd/list'
+import { NzTypographyModule } from 'ng-zorro-antd/typography'
+import { NzIconModule } from 'ng-zorro-antd/icon'
+import { NzToolTipModule } from 'ng-zorro-antd/tooltip'
+import { isNonNulled } from 'rxjs-etc'
+import { NzSpaceModule } from 'ng-zorro-antd/space'
+import { NzMessageModule, NzMessageService } from 'ng-zorro-antd/message'
+
+@Component({
+ imports: [
+ CommonModule,
+ FormsModule,
+ NzFormModule,
+ ReactiveFormsModule,
+ NzCardModule,
+ NzAlertModule,
+ NzRadioModule,
+ NzButtonModule,
+ NzSpinModule,
+ NzListModule,
+ NzTypographyModule,
+ NzIconModule,
+ NzToolTipModule,
+ NzSpaceModule,
+ NzMessageModule,
+ CvcFormErrorsAlertModule,
+ ],
+ standalone: true,
+ selector: 'cvc-user-api-keys-form',
+ templateUrl: './user-api-keys.form.html',
+ styleUrls: ['./user-api-keys.form.less'],
+})
+export class CvcUserApiKeysForm implements OnDestroy {
+ success: boolean = false
+ errorMessages: string[] = []
+ successMessage: string = ''
+ loading: boolean = false
+
+ private destroy$ = new Subject()
+
+ apiKeys: WritableSignal = signal([])
+ newApiKey: WritableSignal> = signal(undefined)
+
+ revokeApiKeyMutator: MutatorWithState<
+ RevokeApiKeyGQL,
+ RevokeApiKeyMutation,
+ RevokeApiKeyMutationVariables
+ >
+
+ generateApiKeyMutator: MutatorWithState<
+ GenerateApiKeyGQL,
+ GenerateApiKeyMutation,
+ GenerateApiKeyMutationVariables
+ >
+
+ constructor(
+ private generateApiKeyGql: GenerateApiKeyGQL,
+ private revokeApiKeyGql: RevokeApiKeyGQL,
+ private apiKeysGql: ApiKeysGQL,
+ private message: NzMessageService,
+ networkErrorService: NetworkErrorsService
+ ) {
+ this.generateApiKeyMutator = new MutatorWithState(networkErrorService)
+ this.revokeApiKeyMutator = new MutatorWithState(networkErrorService)
+ apiKeysGql
+ .watch()
+ .valueChanges.pipe(filter(isNonNulled), takeUntil(this.destroy$))
+ .subscribe(({ data }) => {
+ if (data?.viewer?.apiKeys) {
+ this.apiKeys.set(data.viewer.apiKeys)
+ }
+ })
+ }
+
+ revokeKey(id: number) {
+ let input: RevokeApiKeyInput = {
+ id: id,
+ }
+
+ let state = this.revokeApiKeyMutator.mutate(
+ this.revokeApiKeyGql,
+ { input: input },
+ { refetchQueries: [{ query: this.apiKeysGql.document }] }
+ )
+ this.manageState(state, 'API Key Revoked Successfully')
+ }
+
+ generateKey() {
+ let input: GenerateApiKeyInput = {}
+
+ let state = this.generateApiKeyMutator.mutate(
+ this.generateApiKeyGql,
+ { input: input },
+ {},
+ (data) => {
+ this.newApiKey.set(data.generateApiKey?.apiKey)
+ }
+ )
+
+ this.manageState(
+ state,
+ 'API Key Created. Store It Somewhere Safe, You Will Not Be Able To See It Again'
+ )
+ }
+
+ copyKey(key?: string) {
+ if (key) {
+ navigator.clipboard.writeText(key)
+ this.message.info('Copied')
+ }
+ }
+
+ manageState(state: MutationState, message: string) {
+ this.errorMessages = []
+
+ state.submitSuccess$.pipe(takeUntil(this.destroy$)).subscribe((res) => {
+ if (res) {
+ this.success = true
+ this.successMessage = message
+ }
+ })
+
+ state.submitError$.pipe(takeUntil(this.destroy$)).subscribe((errs) => {
+ if (errs) {
+ this.errorMessages = errs
+ this.success = false
+ this.successMessage = ''
+ }
+ })
+
+ state.isSubmitting$.pipe(takeUntil(this.destroy$)).subscribe((loading) => {
+ this.loading = loading
+ })
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next()
+ this.destroy$.complete()
+ }
+}
diff --git a/client/src/app/forms/components/user-api-keys/user-api-keys.mutation.gql b/client/src/app/forms/components/user-api-keys/user-api-keys.mutation.gql
new file mode 100644
index 000000000..d39a3385e
--- /dev/null
+++ b/client/src/app/forms/components/user-api-keys/user-api-keys.mutation.gql
@@ -0,0 +1,28 @@
+mutation GenerateApiKey($input: GenerateApiKeyInput!) {
+ generateApiKey(input: $input) {
+ apiKey {
+ id
+ reminder
+ createdAt
+ token
+ }
+ }
+}
+
+mutation RevokeApiKey($input: RevokeApiKeyInput!) {
+ revokeApiKey(input: $input) {
+ success
+ }
+}
+
+query ApiKeys {
+ viewer {
+ apiKeys {
+ id
+ reminder
+ createdAt
+ token
+ }
+ }
+}
+
diff --git a/client/src/app/generated/civic.apollo-helpers.ts b/client/src/app/generated/civic.apollo-helpers.ts
index 877180504..adb1481b5 100644
--- a/client/src/app/generated/civic.apollo-helpers.ts
+++ b/client/src/app/generated/civic.apollo-helpers.ts
@@ -85,6 +85,13 @@ export type AdvancedSearchResultFieldPolicy = {
resultIds?: FieldPolicy | FieldReadFunction,
searchEndpoint?: FieldPolicy | FieldReadFunction
};
+export type ApiKeyKeySpecifier = ('createdAt' | 'id' | 'reminder' | 'token' | ApiKeyKeySpecifier)[];
+export type ApiKeyFieldPolicy = {
+ createdAt?: FieldPolicy | FieldReadFunction,
+ id?: FieldPolicy | FieldReadFunction,
+ reminder?: FieldPolicy | FieldReadFunction,
+ token?: FieldPolicy | FieldReadFunction
+};
export type AssertionKeySpecifier = ('acceptanceEvent' | 'acmgCodes' | 'ampLevel' | 'assertionDirection' | 'assertionType' | 'clingenCodes' | 'comments' | 'description' | 'disease' | 'events' | 'evidenceItems' | 'evidenceItemsCount' | 'fdaCompanionTest' | 'fdaCompanionTestLastUpdated' | 'flagged' | 'flags' | 'id' | 'lastAcceptedRevisionEvent' | 'lastCommentEvent' | 'lastSubmittedRevisionEvent' | 'link' | 'molecularProfile' | 'name' | 'nccnGuideline' | 'nccnGuidelineVersion' | 'openRevisionCount' | 'phenotypes' | 'regulatoryApproval' | 'regulatoryApprovalLastUpdated' | 'rejectionEvent' | 'revisions' | 'significance' | 'status' | 'submissionActivity' | 'submissionEvent' | 'summary' | 'therapies' | 'therapyInteractionType' | 'variantOrigin' | AssertionKeySpecifier)[];
export type AssertionFieldPolicy = {
acceptanceEvent?: FieldPolicy | FieldReadFunction,
@@ -377,8 +384,9 @@ export type BrowseTherapyEdgeFieldPolicy = {
cursor?: FieldPolicy | FieldReadFunction,
node?: FieldPolicy | FieldReadFunction
};
-export type BrowseUserKeySpecifier = ('areaOfExpertise' | 'bio' | 'country' | 'displayName' | 'email' | 'events' | 'evidenceCount' | 'facebookProfile' | 'id' | 'linkedinProfile' | 'mostRecentActivityTimestamp' | 'mostRecentConflictOfInterestStatement' | 'mostRecentEvent' | 'mostRecentOrganizationId' | 'name' | 'notifications' | 'orcid' | 'organizations' | 'profileImagePath' | 'ranks' | 'revisionCount' | 'role' | 'statsHash' | 'twitterHandle' | 'url' | 'username' | BrowseUserKeySpecifier)[];
+export type BrowseUserKeySpecifier = ('apiKeys' | 'areaOfExpertise' | 'bio' | 'country' | 'displayName' | 'email' | 'events' | 'evidenceCount' | 'facebookProfile' | 'id' | 'linkedinProfile' | 'mostRecentActivityTimestamp' | 'mostRecentConflictOfInterestStatement' | 'mostRecentEvent' | 'mostRecentOrganizationId' | 'name' | 'notifications' | 'orcid' | 'organizations' | 'profileImagePath' | 'ranks' | 'revisionCount' | 'role' | 'statsHash' | 'twitterHandle' | 'url' | 'username' | BrowseUserKeySpecifier)[];
export type BrowseUserFieldPolicy = {
+ apiKeys?: FieldPolicy | FieldReadFunction,
areaOfExpertise?: FieldPolicy | FieldReadFunction,
bio?: FieldPolicy | FieldReadFunction,
country?: FieldPolicy | FieldReadFunction,
@@ -1348,6 +1356,12 @@ export type GeneVariantEdgeFieldPolicy = {
cursor?: FieldPolicy | FieldReadFunction,
node?: FieldPolicy | FieldReadFunction
};
+export type GenerateApiKeyPayloadKeySpecifier = ('apiKey' | 'clientMutationId' | 'user' | GenerateApiKeyPayloadKeySpecifier)[];
+export type GenerateApiKeyPayloadFieldPolicy = {
+ apiKey?: FieldPolicy | FieldReadFunction,
+ clientMutationId?: FieldPolicy | FieldReadFunction,
+ user?: FieldPolicy | FieldReadFunction
+};
export type LeaderboardOrganizationKeySpecifier = ('actionCount' | 'description' | 'eventCount' | 'events' | 'id' | 'memberCount' | 'members' | 'mostRecentActivityTimestamp' | 'name' | 'orgAndSuborgsStatsHash' | 'orgStatsHash' | 'profileImagePath' | 'rank' | 'ranks' | 'subGroups' | 'url' | LeaderboardOrganizationKeySpecifier)[];
export type LeaderboardOrganizationFieldPolicy = {
actionCount?: FieldPolicy | FieldReadFunction,
@@ -1385,9 +1399,10 @@ export type LeaderboardRankFieldPolicy = {
actionCount?: FieldPolicy | FieldReadFunction,
rank?: FieldPolicy | FieldReadFunction
};
-export type LeaderboardUserKeySpecifier = ('actionCount' | 'areaOfExpertise' | 'bio' | 'country' | 'displayName' | 'email' | 'events' | 'facebookProfile' | 'id' | 'linkedinProfile' | 'mostRecentActivityTimestamp' | 'mostRecentConflictOfInterestStatement' | 'mostRecentEvent' | 'mostRecentOrganizationId' | 'name' | 'notifications' | 'orcid' | 'organizations' | 'profileImagePath' | 'rank' | 'ranks' | 'role' | 'statsHash' | 'twitterHandle' | 'url' | 'username' | LeaderboardUserKeySpecifier)[];
+export type LeaderboardUserKeySpecifier = ('actionCount' | 'apiKeys' | 'areaOfExpertise' | 'bio' | 'country' | 'displayName' | 'email' | 'events' | 'facebookProfile' | 'id' | 'linkedinProfile' | 'mostRecentActivityTimestamp' | 'mostRecentConflictOfInterestStatement' | 'mostRecentEvent' | 'mostRecentOrganizationId' | 'name' | 'notifications' | 'orcid' | 'organizations' | 'profileImagePath' | 'rank' | 'ranks' | 'role' | 'statsHash' | 'twitterHandle' | 'url' | 'username' | LeaderboardUserKeySpecifier)[];
export type LeaderboardUserFieldPolicy = {
actionCount?: FieldPolicy | FieldReadFunction,
+ apiKeys?: FieldPolicy | FieldReadFunction,
areaOfExpertise?: FieldPolicy | FieldReadFunction,
bio?: FieldPolicy | FieldReadFunction,
country?: FieldPolicy | FieldReadFunction,
@@ -1586,7 +1601,7 @@ export type MolecularProfileTextSegmentKeySpecifier = ('text' | MolecularProfile
export type MolecularProfileTextSegmentFieldPolicy = {
text?: FieldPolicy | FieldReadFunction
};
-export type MutationKeySpecifier = ('acceptRevisions' | 'addComment' | 'addDisease' | 'addRemoteCitation' | 'addTherapy' | 'createFeature' | 'createFusionFeature' | 'createFusionVariant' | 'createMolecularProfile' | 'createVariant' | 'deleteComment' | 'deprecateComplexMolecularProfile' | 'deprecateFeature' | 'deprecateVariant' | 'editUser' | 'flagEntity' | 'moderateAssertion' | 'moderateEvidenceItem' | 'rejectRevisions' | 'resolveFlag' | 'submitAssertion' | 'submitEvidence' | 'submitVariantGroup' | 'subscribe' | 'suggestAssertionRevision' | 'suggestEvidenceItemRevision' | 'suggestFactorRevision' | 'suggestFactorVariantRevision' | 'suggestFusionRevision' | 'suggestFusionVariantRevision' | 'suggestGeneRevision' | 'suggestGeneVariantRevision' | 'suggestMolecularProfileRevision' | 'suggestSource' | 'suggestVariantGroupRevision' | 'unsubscribe' | 'updateCoi' | 'updateNotificationStatus' | 'updateSourceSuggestionStatus' | MutationKeySpecifier)[];
+export type MutationKeySpecifier = ('acceptRevisions' | 'addComment' | 'addDisease' | 'addRemoteCitation' | 'addTherapy' | 'createFeature' | 'createFusionFeature' | 'createFusionVariant' | 'createMolecularProfile' | 'createVariant' | 'deleteComment' | 'deprecateComplexMolecularProfile' | 'deprecateFeature' | 'deprecateVariant' | 'editUser' | 'flagEntity' | 'generateApiKey' | 'moderateAssertion' | 'moderateEvidenceItem' | 'rejectRevisions' | 'resolveFlag' | 'revokeApiKey' | 'submitAssertion' | 'submitEvidence' | 'submitVariantGroup' | 'subscribe' | 'suggestAssertionRevision' | 'suggestEvidenceItemRevision' | 'suggestFactorRevision' | 'suggestFactorVariantRevision' | 'suggestFusionRevision' | 'suggestFusionVariantRevision' | 'suggestGeneRevision' | 'suggestGeneVariantRevision' | 'suggestMolecularProfileRevision' | 'suggestSource' | 'suggestVariantGroupRevision' | 'unsubscribe' | 'updateCoi' | 'updateNotificationStatus' | 'updateSourceSuggestionStatus' | MutationKeySpecifier)[];
export type MutationFieldPolicy = {
acceptRevisions?: FieldPolicy | FieldReadFunction,
addComment?: FieldPolicy | FieldReadFunction,
@@ -1604,10 +1619,12 @@ export type MutationFieldPolicy = {
deprecateVariant?: FieldPolicy | FieldReadFunction,
editUser?: FieldPolicy | FieldReadFunction,
flagEntity?: FieldPolicy | FieldReadFunction,
+ generateApiKey?: FieldPolicy | FieldReadFunction,
moderateAssertion?: FieldPolicy | FieldReadFunction,
moderateEvidenceItem?: FieldPolicy | FieldReadFunction,
rejectRevisions?: FieldPolicy | FieldReadFunction,
resolveFlag?: FieldPolicy | FieldReadFunction,
+ revokeApiKey?: FieldPolicy | FieldReadFunction,
submitAssertion?: FieldPolicy | FieldReadFunction,
submitEvidence?: FieldPolicy | FieldReadFunction,
submitVariantGroup?: FieldPolicy | FieldReadFunction,
@@ -2043,6 +2060,11 @@ export type RevisionSetFieldPolicy = {
revisions?: FieldPolicy | FieldReadFunction,
updatedAt?: FieldPolicy | FieldReadFunction
};
+export type RevokeApiKeyPayloadKeySpecifier = ('clientMutationId' | 'success' | RevokeApiKeyPayloadKeySpecifier)[];
+export type RevokeApiKeyPayloadFieldPolicy = {
+ clientMutationId?: FieldPolicy | FieldReadFunction,
+ success?: FieldPolicy | FieldReadFunction
+};
export type ScalarFieldKeySpecifier = ('value' | ScalarFieldKeySpecifier)[];
export type ScalarFieldFieldPolicy = {
value?: FieldPolicy | FieldReadFunction
@@ -2418,8 +2440,9 @@ export type UpdateSourceSuggestionStatusPayloadFieldPolicy = {
clientMutationId?: FieldPolicy | FieldReadFunction,
sourceSuggestion?: FieldPolicy | FieldReadFunction
};
-export type UserKeySpecifier = ('areaOfExpertise' | 'bio' | 'country' | 'displayName' | 'email' | 'events' | 'facebookProfile' | 'id' | 'linkedinProfile' | 'mostRecentActivityTimestamp' | 'mostRecentConflictOfInterestStatement' | 'mostRecentEvent' | 'mostRecentOrg' | 'mostRecentOrganizationId' | 'name' | 'notifications' | 'orcid' | 'organizations' | 'profileImagePath' | 'ranks' | 'role' | 'statsHash' | 'twitterHandle' | 'url' | 'username' | UserKeySpecifier)[];
+export type UserKeySpecifier = ('apiKeys' | 'areaOfExpertise' | 'bio' | 'country' | 'displayName' | 'email' | 'events' | 'facebookProfile' | 'id' | 'linkedinProfile' | 'mostRecentActivityTimestamp' | 'mostRecentConflictOfInterestStatement' | 'mostRecentEvent' | 'mostRecentOrg' | 'mostRecentOrganizationId' | 'name' | 'notifications' | 'orcid' | 'organizations' | 'profileImagePath' | 'ranks' | 'role' | 'statsHash' | 'twitterHandle' | 'url' | 'username' | UserKeySpecifier)[];
export type UserFieldPolicy = {
+ apiKeys?: FieldPolicy | FieldReadFunction,
areaOfExpertise?: FieldPolicy | FieldReadFunction,
bio?: FieldPolicy | FieldReadFunction,
country?: FieldPolicy | FieldReadFunction,
@@ -2660,6 +2683,10 @@ export type StrictTypedTypePolicies = {
keyFields?: false | AdvancedSearchResultKeySpecifier | (() => undefined | AdvancedSearchResultKeySpecifier),
fields?: AdvancedSearchResultFieldPolicy,
},
+ ApiKey?: Omit & {
+ keyFields?: false | ApiKeyKeySpecifier | (() => undefined | ApiKeyKeySpecifier),
+ fields?: ApiKeyFieldPolicy,
+ },
Assertion?: Omit & {
keyFields?: false | AssertionKeySpecifier | (() => undefined | AssertionKeySpecifier),
fields?: AssertionFieldPolicy,
@@ -3132,6 +3159,10 @@ export type StrictTypedTypePolicies = {
keyFields?: false | GeneVariantEdgeKeySpecifier | (() => undefined | GeneVariantEdgeKeySpecifier),
fields?: GeneVariantEdgeFieldPolicy,
},
+ GenerateApiKeyPayload?: Omit & {
+ keyFields?: false | GenerateApiKeyPayloadKeySpecifier | (() => undefined | GenerateApiKeyPayloadKeySpecifier),
+ fields?: GenerateApiKeyPayloadFieldPolicy,
+ },
LeaderboardOrganization?: Omit & {
keyFields?: false | LeaderboardOrganizationKeySpecifier | (() => undefined | LeaderboardOrganizationKeySpecifier),
fields?: LeaderboardOrganizationFieldPolicy,
@@ -3356,6 +3387,10 @@ export type StrictTypedTypePolicies = {
keyFields?: false | RevisionSetKeySpecifier | (() => undefined | RevisionSetKeySpecifier),
fields?: RevisionSetFieldPolicy,
},
+ RevokeApiKeyPayload?: Omit & {
+ keyFields?: false | RevokeApiKeyPayloadKeySpecifier | (() => undefined | RevokeApiKeyPayloadKeySpecifier),
+ fields?: RevokeApiKeyPayloadFieldPolicy,
+ },
ScalarField?: Omit & {
keyFields?: false | ScalarFieldKeySpecifier | (() => undefined | ScalarFieldKeySpecifier),
fields?: ScalarFieldFieldPolicy,
diff --git a/client/src/app/generated/civic.apollo.ts b/client/src/app/generated/civic.apollo.ts
index 255fe12d6..85aee9a92 100644
--- a/client/src/app/generated/civic.apollo.ts
+++ b/client/src/app/generated/civic.apollo.ts
@@ -129,7 +129,6 @@ export enum ActivitySubjectInput {
Assertion = 'ASSERTION',
Comment = 'COMMENT',
EvidenceItem = 'EVIDENCE_ITEM',
- ExonCoordinate = 'EXON_COORDINATE',
Feature = 'FEATURE',
Flag = 'FLAG',
MolecularProfile = 'MOLECULAR_PROFILE',
@@ -138,7 +137,6 @@ export enum ActivitySubjectInput {
Source = 'SOURCE',
SourceSuggestion = 'SOURCE_SUGGESTION',
Variant = 'VARIANT',
- VariantCoordinate = 'VARIANT_COORDINATE',
VariantGroup = 'VARIANT_GROUP'
}
@@ -270,6 +268,14 @@ export enum AmpLevel {
TierILevelB = 'TIER_I_LEVEL_B'
}
+export type ApiKey = {
+ __typename: 'ApiKey';
+ createdAt?: Maybe;
+ id: Scalars['Int']['output'];
+ reminder: Scalars['String']['output'];
+ token?: Maybe;
+};
+
export enum AreaOfExpertise {
ClinicalScientist = 'CLINICAL_SCIENTIST',
PatientAdvocate = 'PATIENT_ADVOCATE',
@@ -874,6 +880,7 @@ export type BrowseTherapyEdge = {
export type BrowseUser = {
__typename: 'BrowseUser';
+ apiKeys: Array;
areaOfExpertise?: Maybe;
bio?: Maybe;
country?: Maybe;
@@ -3479,6 +3486,22 @@ export type GeneVariantFields = {
variantTypeIds: Array;
};
+/** Autogenerated input type of GenerateApiKey */
+export type GenerateApiKeyInput = {
+ /** A unique identifier for the client performing the mutation. */
+ clientMutationId?: InputMaybe;
+};
+
+/** Autogenerated return type of GenerateApiKey. */
+export type GenerateApiKeyPayload = {
+ __typename: 'GenerateApiKeyPayload';
+ /** The newly created API key. */
+ apiKey?: Maybe;
+ /** A unique identifier for the client performing the mutation. */
+ clientMutationId?: Maybe;
+ user?: Maybe;
+};
+
export type IntSearchInput = {
comparisonOperator: IntSearchOperator;
value: Scalars['Int']['input'];
@@ -3568,6 +3591,7 @@ export type LeaderboardRank = {
export type LeaderboardUser = {
__typename: 'LeaderboardUser';
actionCount: Scalars['Int']['output'];
+ apiKeys: Array;
areaOfExpertise?: Maybe;
bio?: Maybe;
country?: Maybe;
@@ -4077,6 +4101,8 @@ export type Mutation = {
editUser?: Maybe;
/** Flag an entity to signal to the editorial team that you believe there is an issue with it. */
flagEntity?: Maybe;
+ /** Generate a new API key for the current user. */
+ generateApiKey?: Maybe;
/** Perform moderation actions on an assertion such as accepting, rejecting, or reverting. */
moderateAssertion?: Maybe;
/** Perform moderation actions on an evidence item such as accepting, rejecting, or reverting. */
@@ -4093,6 +4119,8 @@ export type Mutation = {
* of interest statements can resolve other flags.
*/
resolveFlag?: Maybe;
+ /** Revoke an API key for the current user. */
+ revokeApiKey?: Maybe;
/** Propose adding a new Assertion to the CIViC database. */
submitAssertion?: Maybe;
/** Propose adding a new EvidenceItem to the CIViC database. */
@@ -4214,6 +4242,11 @@ export type MutationFlagEntityArgs = {
};
+export type MutationGenerateApiKeyArgs = {
+ input: GenerateApiKeyInput;
+};
+
+
export type MutationModerateAssertionArgs = {
input: ModerateAssertionInput;
};
@@ -4234,6 +4267,11 @@ export type MutationResolveFlagArgs = {
};
+export type MutationRevokeApiKeyArgs = {
+ input: RevokeApiKeyInput;
+};
+
+
export type MutationSubmitAssertionArgs = {
input: SubmitAssertionInput;
};
@@ -5899,6 +5937,23 @@ export enum RevisionStatus {
Superseded = 'SUPERSEDED'
}
+/** Autogenerated input type of RevokeApiKey */
+export type RevokeApiKeyInput = {
+ /** A unique identifier for the client performing the mutation. */
+ clientMutationId?: InputMaybe;
+ /** ID of the API key to be revoked. */
+ id: Scalars['Int']['input'];
+};
+
+/** Autogenerated return type of RevokeApiKey. */
+export type RevokeApiKeyPayload = {
+ __typename: 'RevokeApiKeyPayload';
+ /** A unique identifier for the client performing the mutation. */
+ clientMutationId?: Maybe;
+ /** Indicates whether the API key was successfully revoked. */
+ success: Scalars['Boolean']['output'];
+};
+
export type ScalarField = {
__typename: 'ScalarField';
value?: Maybe;
@@ -7055,6 +7110,7 @@ export type UpdateSourceSuggestionStatusPayload = {
export type User = {
__typename: 'User';
+ apiKeys: Array;
areaOfExpertise?: Maybe;
bio?: Maybe;
country?: Maybe;
@@ -8896,6 +8952,25 @@ export type UpdateSourceSuggestionMutationVariables = Exact<{
export type UpdateSourceSuggestionMutation = { __typename: 'Mutation', updateSourceSuggestionStatus?: { __typename: 'UpdateSourceSuggestionStatusPayload', sourceSuggestion: { __typename: 'SourceSuggestion', id: number, status: SourceSuggestionStatus } } | undefined };
+export type GenerateApiKeyMutationVariables = Exact<{
+ input: GenerateApiKeyInput;
+}>;
+
+
+export type GenerateApiKeyMutation = { __typename: 'Mutation', generateApiKey?: { __typename: 'GenerateApiKeyPayload', apiKey?: { __typename: 'ApiKey', id: number, reminder: string, createdAt?: any | undefined, token?: string | undefined } | undefined } | undefined };
+
+export type RevokeApiKeyMutationVariables = Exact<{
+ input: RevokeApiKeyInput;
+}>;
+
+
+export type RevokeApiKeyMutation = { __typename: 'Mutation', revokeApiKey?: { __typename: 'RevokeApiKeyPayload', success: boolean } | undefined };
+
+export type ApiKeysQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type ApiKeysQuery = { __typename: 'Query', viewer?: { __typename: 'User', apiKeys: Array<{ __typename: 'ApiKey', id: number, reminder: string, createdAt?: any | undefined, token?: string | undefined }> } | undefined };
+
export type UpdateCoiMutationVariables = Exact<{
input: UpdateCoiInput;
}>;
@@ -16098,6 +16173,70 @@ export const UpdateSourceSuggestionDocument = gql`
export class UpdateSourceSuggestionGQL extends Apollo.Mutation {
document = UpdateSourceSuggestionDocument;
+ constructor(apollo: Apollo.Apollo) {
+ super(apollo);
+ }
+ }
+export const GenerateApiKeyDocument = gql`
+ mutation GenerateApiKey($input: GenerateApiKeyInput!) {
+ generateApiKey(input: $input) {
+ apiKey {
+ id
+ reminder
+ createdAt
+ token
+ }
+ }
+}
+ `;
+
+ @Injectable({
+ providedIn: 'root'
+ })
+ export class GenerateApiKeyGQL extends Apollo.Mutation {
+ document = GenerateApiKeyDocument;
+
+ constructor(apollo: Apollo.Apollo) {
+ super(apollo);
+ }
+ }
+export const RevokeApiKeyDocument = gql`
+ mutation RevokeApiKey($input: RevokeApiKeyInput!) {
+ revokeApiKey(input: $input) {
+ success
+ }
+}
+ `;
+
+ @Injectable({
+ providedIn: 'root'
+ })
+ export class RevokeApiKeyGQL extends Apollo.Mutation {
+ document = RevokeApiKeyDocument;
+
+ constructor(apollo: Apollo.Apollo) {
+ super(apollo);
+ }
+ }
+export const ApiKeysDocument = gql`
+ query ApiKeys {
+ viewer {
+ apiKeys {
+ id
+ reminder
+ createdAt
+ token
+ }
+ }
+}
+ `;
+
+ @Injectable({
+ providedIn: 'root'
+ })
+ export class ApiKeysGQL extends Apollo.Query {
+ document = ApiKeysDocument;
+
constructor(apollo: Apollo.Apollo) {
super(apollo);
}
diff --git a/client/src/app/generated/server.model.graphql b/client/src/app/generated/server.model.graphql
index 18840fbe5..89c4b1bef 100644
--- a/client/src/app/generated/server.model.graphql
+++ b/client/src/app/generated/server.model.graphql
@@ -371,6 +371,13 @@ enum AmpLevel {
TIER_I_LEVEL_B
}
+type ApiKey {
+ createdAt: ISO8601DateTime
+ id: Int!
+ reminder: String!
+ token: String
+}
+
enum AreaOfExpertise {
CLINICAL_SCIENTIST
PATIENT_ADVOCATE
@@ -1427,6 +1434,7 @@ type BrowseTherapyEdge {
}
type BrowseUser {
+ apiKeys: [ApiKey!]!
areaOfExpertise: AreaOfExpertise
bio: String
country: Country
@@ -6171,6 +6179,32 @@ input GeneVariantFields {
variantTypeIds: [Int!]!
}
+"""
+Autogenerated input type of GenerateApiKey
+"""
+input GenerateApiKeyInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+}
+
+"""
+Autogenerated return type of GenerateApiKey.
+"""
+type GenerateApiKeyPayload {
+ """
+ The newly created API key.
+ """
+ apiKey: ApiKey
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+ user: User
+}
+
"""
An ISO 8601-encoded datetime
"""
@@ -6309,6 +6343,7 @@ type LeaderboardRank {
type LeaderboardUser {
actionCount: Int!
+ apiKeys: [ApiKey!]!
areaOfExpertise: AreaOfExpertise
bio: String
country: Country
@@ -7240,6 +7275,16 @@ type Mutation {
input: FlagEntityInput!
): FlagEntityPayload
+ """
+ Generate a new API key for the current user.
+ """
+ generateApiKey(
+ """
+ Parameters for GenerateApiKey
+ """
+ input: GenerateApiKeyInput!
+ ): GenerateApiKeyPayload
+
"""
Perform moderation actions on an assertion such as accepting, rejecting, or reverting.
"""
@@ -7284,6 +7329,16 @@ type Mutation {
input: ResolveFlagInput!
): ResolveFlagPayload
+ """
+ Revoke an API key for the current user.
+ """
+ revokeApiKey(
+ """
+ Parameters for RevokeApiKey
+ """
+ input: RevokeApiKeyInput!
+ ): RevokeApiKeyPayload
+
"""
Propose adding a new Assertion to the CIViC database.
"""
@@ -10159,6 +10214,36 @@ enum RevisionStatus {
SUPERSEDED
}
+"""
+Autogenerated input type of RevokeApiKey
+"""
+input RevokeApiKeyInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ ID of the API key to be revoked.
+ """
+ id: Int!
+}
+
+"""
+Autogenerated return type of RevokeApiKey.
+"""
+type RevokeApiKeyPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Indicates whether the API key was successfully revoked.
+ """
+ success: Boolean!
+}
+
type ScalarField {
value: String
}
@@ -11879,6 +11964,7 @@ type UpdateSourceSuggestionStatusPayload {
}
type User {
+ apiKeys: [ApiKey!]!
areaOfExpertise: AreaOfExpertise
bio: String
country: Country
diff --git a/client/src/app/generated/server.schema.json b/client/src/app/generated/server.schema.json
index be7d148b1..55789175a 100644
--- a/client/src/app/generated/server.schema.json
+++ b/client/src/app/generated/server.schema.json
@@ -1728,6 +1728,73 @@
],
"possibleTypes": null
},
+ {
+ "kind": "OBJECT",
+ "name": "ApiKey",
+ "description": null,
+ "fields": [
+ {
+ "name": "createdAt",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "ISO8601DateTime",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "reminder",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "token",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [],
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "ENUM",
"name": "AreaOfExpertise",
@@ -6821,6 +6888,30 @@
"name": "BrowseUser",
"description": null,
"fields": [
+ {
+ "name": "apiKeys",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "ApiKey",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "areaOfExpertise",
"description": null,
@@ -28420,6 +28511,76 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "GenerateApiKeyInput",
+ "description": "Autogenerated input type of GenerateApiKey",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "GenerateApiKeyPayload",
+ "description": "Autogenerated return type of GenerateApiKey.",
+ "fields": [
+ {
+ "name": "apiKey",
+ "description": "The newly created API key.",
+ "args": [],
+ "type": {
+ "kind": "OBJECT",
+ "name": "ApiKey",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "user",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [],
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "SCALAR",
"name": "ID",
@@ -29138,6 +29299,30 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "apiKeys",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "ApiKey",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "areaOfExpertise",
"description": null,
@@ -33361,6 +33546,35 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "generateApiKey",
+ "description": "Generate a new API key for the current user.",
+ "args": [
+ {
+ "name": "input",
+ "description": "Parameters for GenerateApiKey",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "GenerateApiKeyInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "GenerateApiKeyPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "moderateAssertion",
"description": "Perform moderation actions on an assertion such as accepting, rejecting, or reverting.",
@@ -33477,6 +33691,35 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "revokeApiKey",
+ "description": "Revoke an API key for the current user.",
+ "args": [
+ {
+ "name": "input",
+ "description": "Parameters for RevokeApiKey",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "RevokeApiKeyInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "RevokeApiKeyPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "submitAssertion",
"description": "Propose adding a new Assertion to the CIViC database.",
@@ -46252,6 +46495,84 @@
],
"possibleTypes": null
},
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "RevokeApiKeyInput",
+ "description": "Autogenerated input type of RevokeApiKey",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "ID of the API key to be revoked.",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "defaultValue": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "RevokeApiKeyPayload",
+ "description": "Autogenerated return type of RevokeApiKey.",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "success",
+ "description": "Indicates whether the API key was successfully revoked.",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [],
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "OBJECT",
"name": "ScalarField",
@@ -53427,6 +53748,30 @@
"name": "User",
"description": null,
"fields": [
+ {
+ "name": "apiKeys",
+ "description": null,
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "ApiKey",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "areaOfExpertise",
"description": null,
diff --git a/client/src/app/views/users/users-detail/users-detail.component.html b/client/src/app/views/users/users-detail/users-detail.component.html
index fe2d624f0..e944f5258 100644
--- a/client/src/app/views/users/users-detail/users-detail.component.html
+++ b/client/src/app/views/users/users-detail/users-detail.component.html
@@ -143,6 +143,15 @@
*nzSpaceItem
(uploadComplete)="profileUploadComplete($event)">
+
@@ -274,7 +283,7 @@
nzTitle="Updated">
{{
user.mostRecentConflictOfInterestStatement.createdAt
- | date : 'shortDate'
+ | date: 'shortDate'
}}
{{
user.mostRecentConflictOfInterestStatement.expiresAt
- | date : 'shortDate'
+ | date: 'shortDate'
}}
-
+
+ Manage API Keys
+
+
+
+
+
"Bearer #{@api_key.token}" }
+ response_data = JSON.parse(@response.body).dig("data", "viewer")
+ assert_equal response_data["id"], @user1.id
+ assert_equal response_data["username"], @user1.username
+ end
+
+ test "getting the viewer with a revoked bearer token" do
+ post "/api/graphql", params: { query: @query_string }, headers: { "Authorization" => "Bearer #{@revoked_api_key.token}" }
+ assert_nil JSON.parse(@response.body).dig("data", "viewer")
+ end
+
+ test "adding a comment without a bearer token" do
+ post "/api/graphql", params: { query: @mutation_string }
+ assert_match (/You must log in to perform this mutation/), JSON.parse(@response.body).dig("errors", 0, "message")
+ end
+
+ test "adding a comment with a valid bearer token" do
+ post "/api/graphql", params: { query: @mutation_string }, headers: { "Authorization" => "Bearer #{@api_key.token}" }
+ response_data = JSON.parse(@response.body).dig("data", "addComment", "comment")
+ assert_not_nil response_data
+ assert_equal response_data["comment"], "This is a test comment"
+ end
+end
diff --git a/server/test/graphql/mutations/generate_and_revoke_api_keys_test.rb b/server/test/graphql/mutations/generate_and_revoke_api_keys_test.rb
new file mode 100644
index 000000000..4a135465f
--- /dev/null
+++ b/server/test/graphql/mutations/generate_and_revoke_api_keys_test.rb
@@ -0,0 +1,77 @@
+class GenerateAndRevokeApiKeysTest < ActiveSupport::TestCase
+ def setup
+ @user = users(:curator)
+ @generate_api_key_mutation = <<-GRAPHQL
+ mutation {
+ generateApiKey(input: {}) {
+ apiKey {
+ id
+ token
+ }
+ user {
+ id
+ apiKeys {
+ id
+ token
+ }
+ }
+ }
+ }
+ GRAPHQL
+
+ @revoke_api_key_mutation = <<-GRAPHQL
+ mutation($id: Int!) {
+ revokeApiKey(input: { id: $id }) {
+ success
+ }
+ }
+ GRAPHQL
+
+ @get_api_keys_query = <<-GRAPHQL
+ query {
+ viewer {
+ apiKeys {
+ id
+ reminder
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ test "calling the generate api key mutation results in two associated api keys" do
+ # Generate the first API key
+ response = Civic2Schema.execute(@generate_api_key_mutation, context: { current_user: @user })
+ assert response.dig("data", "generateApiKey", "apiKey").present?
+
+ # Generate the second API key
+ response = Civic2Schema.execute(@generate_api_key_mutation, context: { current_user: @user })
+ assert response.dig("data", "generateApiKey", "apiKey").present?
+
+ # Fetch the user's API keys
+ response = Civic2Schema.execute(@get_api_keys_query, context: { current_user: @user })
+ api_keys = response.dig("data", "viewer", "apiKeys")
+ assert_equal 2, api_keys.size
+ end
+
+ test "calling the revoke api key mutation sets the status of the api key to revoked" do
+ # Generate an API key
+ response = Civic2Schema.execute(@generate_api_key_mutation, context: { current_user: @user })
+ api_key_id = response.dig("data", "generateApiKey", "apiKey", "id")
+ assert api_key_id.present?
+
+ # Revoke the API key
+ response = Civic2Schema.execute(@revoke_api_key_mutation, variables: { id: api_key_id }, context: { current_user: @user })
+ assert response.dig("data", "revokeApiKey", "success")
+
+ # Fetch the user's API keys
+ response = Civic2Schema.execute(@get_api_keys_query, context: { current_user: @user })
+ api_keys = response.dig("data", "viewer", "apiKeys")
+ revoked_key = api_keys.find { |key| key["id"] == api_key_id }
+ assert_nil revoked_key
+
+ # Verify the API key status in the database
+ api_key = ApiKey.find(api_key_id)
+ assert api_key.revoked
+ end
+end