From e786f4bd1d3dbf6e9fe73b7b101d8f92750ff6ca Mon Sep 17 00:00:00 2001 From: Matthew Zikherman Date: Mon, 18 Nov 2024 17:17:23 -0500 Subject: [PATCH] feat: add unstitched second factor mutations --- _schemaV2.graphql | 147 +++++++----------- package.json | 4 +- .../loaders_with_authentication/gravity.ts | 25 +++ src/lib/stitching/gravity/schema.ts | 45 ++++++ src/schema/v2/me/index.ts | 5 +- .../mutations/createAppSecondFactor.ts | 57 +++++++ .../mutations/createBackupSecondFactors.ts | 49 ++++++ .../mutations/createSmsSecondFactor.ts | 62 ++++++++ .../mutations/deliverSecondFactor.ts | 42 +++++ .../mutations/disableSecondFactor.ts | 50 ++++++ .../mutations/enableSecondFactor.ts | 65 ++++++++ .../mutations/updateAppSecondFactor.ts | 54 +++++++ .../mutations/updateSmsSecondFactor.ts | 59 +++++++ .../me/{ => secondFactors}/secondFactors.ts | 126 ++++++++++++++- src/schema/v2/schema.ts | 27 +++- 15 files changed, 718 insertions(+), 99 deletions(-) create mode 100644 src/schema/v2/me/secondFactors/mutations/createAppSecondFactor.ts create mode 100644 src/schema/v2/me/secondFactors/mutations/createBackupSecondFactors.ts create mode 100644 src/schema/v2/me/secondFactors/mutations/createSmsSecondFactor.ts create mode 100644 src/schema/v2/me/secondFactors/mutations/deliverSecondFactor.ts create mode 100644 src/schema/v2/me/secondFactors/mutations/disableSecondFactor.ts create mode 100644 src/schema/v2/me/secondFactors/mutations/enableSecondFactor.ts create mode 100644 src/schema/v2/me/secondFactors/mutations/updateAppSecondFactor.ts create mode 100644 src/schema/v2/me/secondFactors/mutations/updateSmsSecondFactor.ts rename src/schema/v2/me/{ => secondFactors}/secondFactors.ts (50%) diff --git a/_schemaV2.graphql b/_schemaV2.graphql index aa275e6705..c3169d2a66 100644 --- a/_schemaV2.graphql +++ b/_schemaV2.graphql @@ -1162,27 +1162,33 @@ type AnalyticsVisitorsByReferral { value: Int! } -# App Authenticator Two-Factor Authentication factor type AppSecondFactor implements SecondFactor { - createdAt: ISO8601DateTime! - disabledAt: ISO8601DateTime + disabledAt( + format: String + + # A tz database time zone, otherwise falls back to "X-TIMEZONE" header. See http://www.iana.org/time-zones, https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + timezone: String + ): String enabled: Boolean! - enabledAt: ISO8601DateTime + enabledAt( + format: String + + # A tz database time zone, otherwise falls back to "X-TIMEZONE" header. See http://www.iana.org/time-zones, https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + timezone: String + ): String + + # A type-specific Gravity Mongo Document ID. internalID: ID! kind: SecondFactorKind! name: String otpProvisioningURI: String otpSecret: String - updatedAt: ISO8601DateTime! } -# Second factor input attributes input AppSecondFactorAttributes { - # Name of the second factor name: String } -# An app second factor or errors union AppSecondFactorOrErrorsUnion = AppSecondFactor | Errors type Article implements Node { @@ -3751,23 +3757,31 @@ type Author { twitterHandle: String } -# Backup Two-Factor Authentication factor type BackupSecondFactor implements SecondFactor { code: String! - createdAt: ISO8601DateTime! - disabledAt: ISO8601DateTime + disabledAt( + format: String + + # A tz database time zone, otherwise falls back to "X-TIMEZONE" header. See http://www.iana.org/time-zones, https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + timezone: String + ): String enabled: Boolean! - enabledAt: ISO8601DateTime + enabledAt( + format: String + + # A tz database time zone, otherwise falls back to "X-TIMEZONE" header. See http://www.iana.org/time-zones, https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + timezone: String + ): String + + # A type-specific Gravity Mongo Document ID. internalID: ID! kind: SecondFactorKind! - updatedAt: ISO8601DateTime! } type BackupSecondFactors { secondFactors: [BackupSecondFactor!]! } -# A list of backup second factors or errors union BackupSecondFactorsOrErrorsUnion = BackupSecondFactors | Errors type BankAccount { @@ -7955,21 +7969,15 @@ input CreateAndSendBackupSecondFactorInput { type CreateAndSendBackupSecondFactorPayload { # A unique identifier for the client performing the mutation. clientMutationId: String - factor: BackupSecondFactor! } -# Autogenerated input type of CreateAppSecondFactor input CreateAppSecondFactorInput { attributes: AppSecondFactorAttributes! - - # A unique identifier for the client performing the mutation. clientMutationId: String password: String! } -# Autogenerated return type of CreateAppSecondFactor type CreateAppSecondFactorPayload { - # A unique identifier for the client performing the mutation. clientMutationId: String secondFactorOrErrors: AppSecondFactorOrErrorsUnion! } @@ -8002,16 +8010,12 @@ type CreateArtistSuccess { union CreateArtistSuccessOrErrorType = CreateArtistFailure | CreateArtistSuccess -# Autogenerated input type of CreateBackupSecondFactors input CreateBackupSecondFactorsInput { - # A unique identifier for the client performing the mutation. clientMutationId: String password: String! } -# Autogenerated return type of CreateBackupSecondFactors type CreateBackupSecondFactorsPayload { - # A unique identifier for the client performing the mutation. clientMutationId: String secondFactorsOrErrors: BackupSecondFactorsOrErrorsUnion! } @@ -8427,18 +8431,13 @@ type CreateSaleAgreementSuccess { saleAgreement: SaleAgreement } -# Autogenerated input type of CreateSmsSecondFactor input CreateSmsSecondFactorInput { attributes: SmsSecondFactorAttributes! - - # A unique identifier for the client performing the mutation. clientMutationId: String password: String! } -# Autogenerated return type of CreateSmsSecondFactor type CreateSmsSecondFactorPayload { - # A unique identifier for the client performing the mutation. clientMutationId: String secondFactorOrErrors: SmsSecondFactorOrErrorsUnion! } @@ -9190,16 +9189,12 @@ type DeleteViewingRoomPayload { viewingRoom: ViewingRoom! } -# Autogenerated input type of DeliverSecondFactor input DeliverSecondFactorInput { - # A unique identifier for the client performing the mutation. clientMutationId: String secondFactorID: ID! } -# Autogenerated return type of DeliverSecondFactor type DeliverSecondFactorPayload { - # A unique identifier for the client performing the mutation. clientMutationId: String secondFactorOrErrors: SecondFactorOrErrorsUnion! } @@ -9267,17 +9262,13 @@ type Device { token: String! } -# Autogenerated input type of DisableSecondFactor input DisableSecondFactorInput { - # A unique identifier for the client performing the mutation. clientMutationId: String password: String! secondFactorID: ID! } -# Autogenerated return type of DisableSecondFactor type DisableSecondFactorPayload { - # A unique identifier for the client performing the mutation. clientMutationId: String secondFactorOrErrors: SecondFactorOrErrorsUnion! } @@ -9425,18 +9416,14 @@ enum EditionSetSorts { PRICE_ASC } -# Autogenerated input type of EnableSecondFactor input EnableSecondFactorInput { - # A unique identifier for the client performing the mutation. clientMutationId: String code: String! password: String! secondFactorID: ID! } -# Autogenerated return type of EnableSecondFactor type EnableSecondFactorPayload { - # A unique identifier for the client performing the mutation. clientMutationId: String recoveryCodes: [String!] secondFactorOrErrors: SecondFactorOrErrorsUnion! @@ -13740,14 +13727,12 @@ type Mutation { input: CreateAndSendBackupSecondFactorInput! ): CreateAndSendBackupSecondFactorPayload createAppSecondFactor( - # Parameters for CreateAppSecondFactor input: CreateAppSecondFactorInput! ): CreateAppSecondFactorPayload # Create an artist createArtist(input: CreateArtistMutationInput!): CreateArtistMutationPayload createBackupSecondFactors( - # Parameters for CreateBackupSecondFactors input: CreateBackupSecondFactorsInput! ): CreateBackupSecondFactorsPayload createBankDebitSetup( @@ -13857,7 +13842,6 @@ type Mutation { input: CreateSaleAgreementMutationInput! ): CreateSaleAgreementMutationPayload createSmsSecondFactor( - # Parameters for CreateSmsSecondFactor input: CreateSmsSecondFactorInput! ): CreateSmsSecondFactorPayload createUserAddress( @@ -14007,11 +13991,9 @@ type Mutation { input: DeleteViewingRoomInput! ): DeleteViewingRoomPayload deliverSecondFactor( - # Parameters for DeliverSecondFactor input: DeliverSecondFactorInput! ): DeliverSecondFactorPayload disableSecondFactor( - # Parameters for DisableSecondFactor input: DisableSecondFactorInput! ): DisableSecondFactorPayload @@ -14020,10 +14002,7 @@ type Mutation { # Updates a Task on the logged in User dismissTask(input: DismissTaskMutationInput!): DismissTaskMutationPayload - enableSecondFactor( - # Parameters for EnableSecondFactor - input: EnableSecondFactorInput! - ): EnableSecondFactorPayload + enableSecondFactor(input: EnableSecondFactorInput!): EnableSecondFactorPayload # Mark sale as ended. endSale(input: EndSaleInput!): EndSalePayload @@ -14163,7 +14142,6 @@ type Mutation { # Create an alert updateAlert(input: updateAlertInput!): updateAlertPayload updateAppSecondFactor( - # Parameters for UpdateAppSecondFactor input: UpdateAppSecondFactorInput! ): UpdateAppSecondFactorPayload @@ -14266,7 +14244,6 @@ type Mutation { input: UpdateSaleAgreementMutationInput! ): UpdateSaleAgreementMutationPayload updateSmsSecondFactor( - # Parameters for UpdateSmsSecondFactor input: UpdateSmsSecondFactorInput! ): UpdateSmsSecondFactorPayload @@ -16359,11 +16336,6 @@ type Query { # Find partners by IDs _unused_gravity_partners(ids: [ID!]!): [DoNotUseThisPartner!] - # List enabled Two-Factor Authentication factors - _unused_gravity_secondFactors( - kinds: [SecondFactorKind!] = [app, sms, backup] - ): [SecondFactor!]! - # List of user's saved addresses _unused_gravity_userAddressConnection( # Returns the elements in the list that come after the specified cursor. @@ -18681,28 +18653,31 @@ type SearchableItem implements Node & Searchable { } interface SecondFactor { - createdAt: ISO8601DateTime! - disabledAt: ISO8601DateTime + disabledAt( + format: String + + # A tz database time zone, otherwise falls back to "X-TIMEZONE" header. See http://www.iana.org/time-zones, https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + timezone: String + ): String enabled: Boolean! - enabledAt: ISO8601DateTime + enabledAt( + format: String + + # A tz database time zone, otherwise falls back to "X-TIMEZONE" header. See http://www.iana.org/time-zones, https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + timezone: String + ): String + + # A type-specific Gravity Mongo Document ID. internalID: ID! kind: SecondFactorKind! - updatedAt: ISO8601DateTime! } -# Two-Factor Authentication (2FA) Method enum SecondFactorKind { - # App authenticator 2FA method app - - # Backup 2FA method backup - - # SMS 2FA method sms } -# A second factor or errors union SecondFactorOrErrorsUnion = AppSecondFactor | Errors | SmsSecondFactor # A piece that can be sold @@ -19222,32 +19197,34 @@ enum ShowSorts { UPDATED_AT_DESC } -# SMS Two-Factor Authentication factor type SmsSecondFactor implements SecondFactor { countryCode: String - createdAt: ISO8601DateTime! - disabledAt: ISO8601DateTime + disabledAt( + format: String + + # A tz database time zone, otherwise falls back to "X-TIMEZONE" header. See http://www.iana.org/time-zones, https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + timezone: String + ): String enabled: Boolean! - enabledAt: ISO8601DateTime + enabledAt( + format: String + + # A tz database time zone, otherwise falls back to "X-TIMEZONE" header. See http://www.iana.org/time-zones, https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + timezone: String + ): String formattedPhoneNumber: String + + # A type-specific Gravity Mongo Document ID. internalID: ID! kind: SecondFactorKind! - lastDeliveredAt: ISO8601DateTime - maskedPhone: String @deprecated(reason: "Use formattedPhoneNumber instead") phoneNumber: String - updatedAt: ISO8601DateTime! } -# SMS second factor input attributes input SmsSecondFactorAttributes { - # ISO 3166 country code for the SMS second factor countryCode: String - - # Phone number of the SMS second factor phoneNumber: String } -# An SMS second factor or errors union SmsSecondFactorOrErrorsUnion = Errors | SmsSecondFactor type SpecialistBio { @@ -19684,18 +19661,13 @@ type UpdateAlertSuccess { alert: Alert } -# Autogenerated input type of UpdateAppSecondFactor input UpdateAppSecondFactorInput { attributes: AppSecondFactorAttributes! - - # A unique identifier for the client performing the mutation. clientMutationId: String secondFactorID: ID! } -# Autogenerated return type of UpdateAppSecondFactor type UpdateAppSecondFactorPayload { - # A unique identifier for the client performing the mutation. clientMutationId: String secondFactorOrErrors: AppSecondFactorOrErrorsUnion! } @@ -20328,18 +20300,13 @@ type UpdateSaleAgreementSuccess { saleAgreement: SaleAgreement } -# Autogenerated input type of UpdateSmsSecondFactor input UpdateSmsSecondFactorInput { attributes: SmsSecondFactorAttributes! - - # A unique identifier for the client performing the mutation. clientMutationId: String secondFactorID: ID! } -# Autogenerated return type of UpdateSmsSecondFactor type UpdateSmsSecondFactorPayload { - # A unique identifier for the client performing the mutation. clientMutationId: String secondFactorOrErrors: SmsSecondFactorOrErrorsUnion! } diff --git a/package.json b/package.json index d2281a020e..97576aa3ca 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "deploy-cloudflare-workers": "yarn wrangler deploy --keep-vars --config=workers/caching/metaphysics-cdn.toml --env production", "dev": "DEBUG=info,warn,error babel-node --extensions '.ts,.js' --inspect index.js", "dump-schema": "babel-node --extensions '.ts,.js' ./scripts/dump-schema.ts", - "dump:local": "USE_UNSTITCHED_SECOND_FACTORS_SCHEMA=false yarn dump-schema _schemaV2.graphql & wait", - "dump:staging": "USE_UNSTITCHED_SECOND_FACTORS_SCHEMA=false node scripts/dump-staging-schema.js", + "dump:local": "USE_UNSTITCHED_SECOND_FACTORS_SCHEMA=true yarn dump-schema _schemaV2.graphql & wait", + "dump:staging": "USE_UNSTITCHED_SECOND_FACTORS_SCHEMA=true node scripts/dump-staging-schema.js", "lint": "eslint . --ext ts", "lint:fix": "eslint . --fix --ext ts", "prepare": "patch-package", diff --git a/src/lib/loaders/loaders_with_authentication/gravity.ts b/src/lib/loaders/loaders_with_authentication/gravity.ts index 2950b5cfda..4ad55bc41a 100644 --- a/src/lib/loaders/loaders_with_authentication/gravity.ts +++ b/src/lib/loaders/loaders_with_authentication/gravity.ts @@ -77,6 +77,31 @@ export default (accessToken, userID, opts) => { {}, { method: "POST" } ), + createSecondFactorLoader: gravityLoader( + "me/second_factors", + {}, + { method: "POST" } + ), + deliverSecondFactor: gravityLoader( + (id) => `me/second_factors/${id}/deliver`, + {}, + { method: "PUT" } + ), + disableSecondFactorLoader: gravityLoader( + (id) => `me/second_factors/${id}`, + {}, + { method: "DELETE" } + ), + enableSecondFactorLoader: gravityLoader( + (id) => `me/second_factors/${id}/enable`, + {}, + { method: "PUT" } + ), + updateSecondFactorLoader: gravityLoader( + (id) => `me/second_factors/${id}`, + {}, + { method: "PUT" } + ), updateArtistCareerHighlightLoader: gravityLoader( (id) => `artist_career_highlight/${id}`, {}, diff --git a/src/lib/stitching/gravity/schema.ts b/src/lib/stitching/gravity/schema.ts index d274c5ea68..07de0f6f43 100644 --- a/src/lib/stitching/gravity/schema.ts +++ b/src/lib/stitching/gravity/schema.ts @@ -5,6 +5,7 @@ import { FilterTypes, RenameTypes, RenameRootFields, + FilterRootFields, } from "graphql-tools" import { readFileSync } from "fs" import config from "config" @@ -54,8 +55,43 @@ export const executableGravitySchema = () => { duplicatedTypes.push("SecondFactor") duplicatedTypes.push("AppSecondFactor") duplicatedTypes.push("BackupSecondFactor") + duplicatedTypes.push("BackupSecondFactors") duplicatedTypes.push("SmsSecondFactor") duplicatedTypes.push("SecondFactorKind") + duplicatedTypes.push("SmsSecondFactorAttributes") + duplicatedTypes.push("SmsSecondFactorOrErrorsUnion") + duplicatedTypes.push("CreateSmsSecondFactorInput") + duplicatedTypes.push("CreateSmsSecondFactorPayload") + duplicatedTypes.push("UpdateSmsSecondFactorInput") + duplicatedTypes.push("UpdateSmsSecondFactorPayload") + duplicatedTypes.push("AppSecondFactorAttributes") + duplicatedTypes.push("AppSecondFactorOrErrorsUnion") + duplicatedTypes.push("CreateAppSecondFactorInput") + duplicatedTypes.push("CreateAppSecondFactorPayload") + duplicatedTypes.push("UpdateAppSecondFactorInput") + duplicatedTypes.push("UpdateAppSecondFactorPayload") + duplicatedTypes.push("BackupSecondFactorsOrErrorsUnion") + duplicatedTypes.push("CreateBackupSecondFactorsInput") + duplicatedTypes.push("CreateBackupSecondFactorsPayload") + duplicatedTypes.push("SecondFactorOrErrorsUnion") + duplicatedTypes.push("DisableSecondFactorInput") + duplicatedTypes.push("DisableSecondFactorPayload") + duplicatedTypes.push("DeliverSecondFactorInput") + duplicatedTypes.push("DeliverSecondFactorPayload") + duplicatedTypes.push("EnableSecondFactorInput") + duplicatedTypes.push("EnableSecondFactorPayload") + } + + const excludedMutations: string[] = [] + if (config.USE_UNSTITCHED_SECOND_FACTORS_SCHEMA) { + excludedMutations.push("createSmsSecondFactor") + excludedMutations.push("updateSmsSecondFactor") + excludedMutations.push("createAppSecondFactor") + excludedMutations.push("updateAppSecondFactor") + excludedMutations.push("createBackupSecondFactors") + excludedMutations.push("disableSecondFactor") + excludedMutations.push("deliverSecondFactor") + excludedMutations.push("enableSecondFactor") } // Types which come from Gravity that are not (yet) needed in MP. @@ -98,5 +134,14 @@ export const executableGravitySchema = () => { return name } }), + new FilterRootFields((operation, name) => { + if (!name) return true + + if (operation === "Mutation") { + return !excludedMutations.includes(name) + } + + return true + }), ]) } diff --git a/src/schema/v2/me/index.ts b/src/schema/v2/me/index.ts index 139b0f899c..f572a77978 100644 --- a/src/schema/v2/me/index.ts +++ b/src/schema/v2/me/index.ts @@ -92,7 +92,10 @@ import { TaskType } from "./task" import { UserInterest } from "./userInterest" import { UserInterestsConnection } from "./userInterestsConnection" import { WatchedLotConnection } from "./watchedLotConnection" -import { SecondFactorInterface, SecondFactorKind } from "./secondFactors" +import { + SecondFactorInterface, + SecondFactorKind, +} from "./secondFactors/secondFactors" /** * @deprecated: Please use the CollectorProfile type instead of adding fields to me directly. diff --git a/src/schema/v2/me/secondFactors/mutations/createAppSecondFactor.ts b/src/schema/v2/me/secondFactors/mutations/createAppSecondFactor.ts new file mode 100644 index 0000000000..940ce87dc0 --- /dev/null +++ b/src/schema/v2/me/secondFactors/mutations/createAppSecondFactor.ts @@ -0,0 +1,57 @@ +import { GraphQLNonNull, GraphQLString } from "graphql" +import { mutationWithClientMutationId } from "graphql-relay" +import { + AppSecondFactorAttributes, + AppSecondFactorMutationResponseOrErrorsType, +} from "../secondFactors" +import { ResolverContext } from "types/graphql" + +export const createAppSecondFactorMutation = mutationWithClientMutationId< + any, + any, + ResolverContext +>({ + name: "CreateAppSecondFactor", + inputFields: { + password: { + type: new GraphQLNonNull(GraphQLString), + }, + attributes: { + type: new GraphQLNonNull(AppSecondFactorAttributes), + }, + }, + outputFields: { + secondFactorOrErrors: { + type: AppSecondFactorMutationResponseOrErrorsType, + resolve: (result) => result, + }, + }, + mutateAndGetPayload: async ( + { password, attributes }, + { createSecondFactorLoader } + ) => { + if (!createSecondFactorLoader) { + throw new Error("You need to be signed in to perform this action") + } + + try { + const data = await createSecondFactorLoader({ + kind: "app", + password, + attributes, + }) + + return data[0] + } catch (error) { + const { body } = error + return { + errors: [ + { + message: body.message ?? body.error, + code: "invalid", + }, + ], + } + } + }, +}) diff --git a/src/schema/v2/me/secondFactors/mutations/createBackupSecondFactors.ts b/src/schema/v2/me/secondFactors/mutations/createBackupSecondFactors.ts new file mode 100644 index 0000000000..909cca4708 --- /dev/null +++ b/src/schema/v2/me/secondFactors/mutations/createBackupSecondFactors.ts @@ -0,0 +1,49 @@ +import { GraphQLNonNull, GraphQLString } from "graphql" +import { mutationWithClientMutationId } from "graphql-relay" +import { BackupSecondFactorsMutationResponseOrErrorsType } from "../secondFactors" +import { ResolverContext } from "types/graphql" + +export const createBackupSecondFactorsMutation = mutationWithClientMutationId< + any, + any, + ResolverContext +>({ + name: "CreateBackupSecondFactors", + inputFields: { + password: { + type: new GraphQLNonNull(GraphQLString), + }, + }, + outputFields: { + secondFactorsOrErrors: { + type: BackupSecondFactorsMutationResponseOrErrorsType, + resolve: (result) => result, + }, + }, + mutateAndGetPayload: async ({ password }, { createSecondFactorLoader }) => { + if (!createSecondFactorLoader) { + throw new Error("You need to be signed in to perform this action") + } + + try { + const data = await createSecondFactorLoader({ + kind: "backup", + password, + }) + + return { + secondFactors: data, + } + } catch (error) { + const { body } = error + return { + errors: [ + { + message: body.message ?? body.error, + code: "invalid", + }, + ], + } + } + }, +}) diff --git a/src/schema/v2/me/secondFactors/mutations/createSmsSecondFactor.ts b/src/schema/v2/me/secondFactors/mutations/createSmsSecondFactor.ts new file mode 100644 index 0000000000..ed0949360b --- /dev/null +++ b/src/schema/v2/me/secondFactors/mutations/createSmsSecondFactor.ts @@ -0,0 +1,62 @@ +import { GraphQLNonNull, GraphQLString } from "graphql" +import { mutationWithClientMutationId } from "graphql-relay" +import { + SmsSecondFactorMutationResponseOrErrorsType, + SmsSecondFactorAttributes, +} from "../secondFactors" +import { ResolverContext } from "types/graphql" + +export const createSmsSecondFactorMutation = mutationWithClientMutationId< + any, + any, + ResolverContext +>({ + name: "CreateSmsSecondFactor", + inputFields: { + password: { + type: new GraphQLNonNull(GraphQLString), + }, + attributes: { + type: new GraphQLNonNull(SmsSecondFactorAttributes), + }, + }, + outputFields: { + secondFactorOrErrors: { + type: SmsSecondFactorMutationResponseOrErrorsType, + resolve: (result) => result, + }, + }, + mutateAndGetPayload: async ( + { password, attributes }, + { createSecondFactorLoader } + ) => { + if (!createSecondFactorLoader) { + throw new Error("You need to be signed in to perform this action") + } + + try { + const snakeCaseAttributes = { + country_code: attributes.countryCode, + phone_number: attributes.phoneNumber, + } + + const data = await createSecondFactorLoader({ + kind: "sms", + password, + attributes: snakeCaseAttributes, + }) + + return data[0] + } catch (error) { + const { body } = error + return { + errors: [ + { + message: body.message ?? body.error, + code: "invalid", + }, + ], + } + } + }, +}) diff --git a/src/schema/v2/me/secondFactors/mutations/deliverSecondFactor.ts b/src/schema/v2/me/secondFactors/mutations/deliverSecondFactor.ts new file mode 100644 index 0000000000..7348247c31 --- /dev/null +++ b/src/schema/v2/me/secondFactors/mutations/deliverSecondFactor.ts @@ -0,0 +1,42 @@ +import { GraphQLID, GraphQLNonNull } from "graphql" +import { mutationWithClientMutationId } from "graphql-relay" +import { SecondFactorOrErrorsUnionType } from "../secondFactors" +import { ResolverContext } from "types/graphql" + +export const deliverSecondFactorMutation = mutationWithClientMutationId< + any, + any, + ResolverContext +>({ + name: "DeliverSecondFactor", + inputFields: { + secondFactorID: { + type: new GraphQLNonNull(GraphQLID), + }, + }, + outputFields: { + secondFactorOrErrors: { + type: SecondFactorOrErrorsUnionType, + resolve: (result) => result, + }, + }, + mutateAndGetPayload: async ({ secondFactorID }, { deliverSecondFactor }) => { + if (!deliverSecondFactor) { + throw new Error("You need to be signed in to perform this action") + } + + try { + return await deliverSecondFactor(secondFactorID) + } catch (error) { + const { body } = error + return { + errors: [ + { + message: body.message ?? body.error, + code: "invalid", + }, + ], + } + } + }, +}) diff --git a/src/schema/v2/me/secondFactors/mutations/disableSecondFactor.ts b/src/schema/v2/me/secondFactors/mutations/disableSecondFactor.ts new file mode 100644 index 0000000000..c932430290 --- /dev/null +++ b/src/schema/v2/me/secondFactors/mutations/disableSecondFactor.ts @@ -0,0 +1,50 @@ +import { GraphQLID, GraphQLNonNull, GraphQLString } from "graphql" +import { mutationWithClientMutationId } from "graphql-relay" +import { SecondFactorOrErrorsUnionType } from "../secondFactors" +import { ResolverContext } from "types/graphql" + +export const disableSecondFactorMutation = mutationWithClientMutationId< + any, + any, + ResolverContext +>({ + name: "DisableSecondFactor", + inputFields: { + password: { + type: new GraphQLNonNull(GraphQLString), + }, + secondFactorID: { + type: new GraphQLNonNull(GraphQLID), + }, + }, + outputFields: { + secondFactorOrErrors: { + type: SecondFactorOrErrorsUnionType, + resolve: (result) => result, + }, + }, + mutateAndGetPayload: async ( + { password, secondFactorID }, + { disableSecondFactorLoader } + ) => { + if (!disableSecondFactorLoader) { + throw new Error("You need to be signed in to perform this action") + } + + try { + return await disableSecondFactorLoader(secondFactorID, { + password, + }) + } catch (error) { + const { body } = error + return { + errors: [ + { + message: body.message ?? body.error, + code: "invalid", + }, + ], + } + } + }, +}) diff --git a/src/schema/v2/me/secondFactors/mutations/enableSecondFactor.ts b/src/schema/v2/me/secondFactors/mutations/enableSecondFactor.ts new file mode 100644 index 0000000000..f6cf820bf9 --- /dev/null +++ b/src/schema/v2/me/secondFactors/mutations/enableSecondFactor.ts @@ -0,0 +1,65 @@ +import { GraphQLID, GraphQLList, GraphQLNonNull, GraphQLString } from "graphql" +import { mutationWithClientMutationId } from "graphql-relay" +import { SecondFactorOrErrorsUnionType } from "../secondFactors" +import { ResolverContext } from "types/graphql" + +export const enableSecondFactorMutation = mutationWithClientMutationId< + any, + any, + ResolverContext +>({ + name: "EnableSecondFactor", + inputFields: { + password: { + type: new GraphQLNonNull(GraphQLString), + }, + code: { + type: new GraphQLNonNull(GraphQLString), + }, + secondFactorID: { + type: new GraphQLNonNull(GraphQLID), + }, + }, + outputFields: { + secondFactorOrErrors: { + type: SecondFactorOrErrorsUnionType, + resolve: (result) => result, + }, + recoveryCodes: { + type: new GraphQLList(new GraphQLNonNull(GraphQLString)), + }, + }, + mutateAndGetPayload: async ( + { password, secondFactorID, code }, + { enableSecondFactorLoader } + ) => { + if (!enableSecondFactorLoader) { + throw new Error("You need to be signed in to perform this action") + } + + try { + const { factor, recovery_codes } = await enableSecondFactorLoader( + secondFactorID, + { + password, + code, + } + ) + + return { + ...factor, + recoveryCodes: recovery_codes, + } + } catch (error) { + const { body } = error + return { + errors: [ + { + message: body.message ?? body.error, + code: "invalid", + }, + ], + } + } + }, +}) diff --git a/src/schema/v2/me/secondFactors/mutations/updateAppSecondFactor.ts b/src/schema/v2/me/secondFactors/mutations/updateAppSecondFactor.ts new file mode 100644 index 0000000000..e2f114bce1 --- /dev/null +++ b/src/schema/v2/me/secondFactors/mutations/updateAppSecondFactor.ts @@ -0,0 +1,54 @@ +import { GraphQLID, GraphQLNonNull } from "graphql" +import { mutationWithClientMutationId } from "graphql-relay" +import { + AppSecondFactorAttributes, + AppSecondFactorMutationResponseOrErrorsType, +} from "../secondFactors" +import { ResolverContext } from "types/graphql" + +export const updateAppSecondFactorMutation = mutationWithClientMutationId< + any, + any, + ResolverContext +>({ + name: "UpdateAppSecondFactor", + inputFields: { + secondFactorID: { + type: new GraphQLNonNull(GraphQLID), + }, + attributes: { + type: new GraphQLNonNull(AppSecondFactorAttributes), + }, + }, + outputFields: { + secondFactorOrErrors: { + type: AppSecondFactorMutationResponseOrErrorsType, + resolve: (result) => result, + }, + }, + mutateAndGetPayload: async ( + { attributes, secondFactorID }, + { updateSecondFactorLoader } + ) => { + if (!updateSecondFactorLoader) { + throw new Error("You need to be signed in to perform this action") + } + + try { + return await updateSecondFactorLoader(secondFactorID, { + kind: "app", + attributes, + }) + } catch (error) { + const { body } = error + return { + errors: [ + { + message: body.message ?? body.error, + code: "invalid", + }, + ], + } + } + }, +}) diff --git a/src/schema/v2/me/secondFactors/mutations/updateSmsSecondFactor.ts b/src/schema/v2/me/secondFactors/mutations/updateSmsSecondFactor.ts new file mode 100644 index 0000000000..dccc8347dc --- /dev/null +++ b/src/schema/v2/me/secondFactors/mutations/updateSmsSecondFactor.ts @@ -0,0 +1,59 @@ +import { GraphQLID, GraphQLNonNull } from "graphql" +import { mutationWithClientMutationId } from "graphql-relay" +import { + SmsSecondFactorMutationResponseOrErrorsType, + SmsSecondFactorAttributes, +} from "../secondFactors" +import { ResolverContext } from "types/graphql" + +export const updateSmsSecondFactorMutation = mutationWithClientMutationId< + any, + any, + ResolverContext +>({ + name: "UpdateSmsSecondFactor", + inputFields: { + secondFactorID: { + type: new GraphQLNonNull(GraphQLID), + }, + attributes: { + type: new GraphQLNonNull(SmsSecondFactorAttributes), + }, + }, + outputFields: { + secondFactorOrErrors: { + type: SmsSecondFactorMutationResponseOrErrorsType, + resolve: (result) => result, + }, + }, + mutateAndGetPayload: async ( + { attributes, secondFactorID }, + { updateSecondFactorLoader } + ) => { + if (!updateSecondFactorLoader) { + throw new Error("You need to be signed in to perform this action") + } + + try { + const snakeCaseAttributes = { + country_code: attributes.countryCode, + phone_number: attributes.phoneNumber, + } + + return await updateSecondFactorLoader(secondFactorID, { + kind: "sms", + attributes: snakeCaseAttributes, + }) + } catch (error) { + const { body } = error + return { + errors: [ + { + message: body.message ?? body.error, + code: "invalid", + }, + ], + } + } + }, +}) diff --git a/src/schema/v2/me/secondFactors.ts b/src/schema/v2/me/secondFactors/secondFactors.ts similarity index 50% rename from src/schema/v2/me/secondFactors.ts rename to src/schema/v2/me/secondFactors/secondFactors.ts index 3a66b5c4b3..a477ed866b 100644 --- a/src/schema/v2/me/secondFactors.ts +++ b/src/schema/v2/me/secondFactors/secondFactors.ts @@ -1,13 +1,17 @@ import { GraphQLBoolean, GraphQLEnumType, + GraphQLID, + GraphQLInputObjectType, GraphQLInterfaceType, + GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString, + GraphQLUnionType, } from "graphql" -import { GravityIDFields } from "../object_identification" -import { date } from "../fields/date" +import { date } from "../../fields/date" +import { ResolverContext } from "types/graphql" export const SecondFactorKind = new GraphQLEnumType({ name: "SecondFactorKind", @@ -31,7 +35,11 @@ interface SharedGravityJSONFields { _id: string } const sharedFields = { - ...GravityIDFields, + internalID: { + description: "A type-specific Gravity Mongo Document ID.", + type: new GraphQLNonNull(GraphQLID), + resolve: ({ _id }) => _id, + }, enabled: { type: new GraphQLNonNull(GraphQLBoolean), resolve: ({ enabled_at }) => !!enabled_at, @@ -98,7 +106,7 @@ export const BackupSecondFactor = new GraphQLObjectType< fields: { ...sharedFields, code: { - type: GraphQLString, + type: new GraphQLNonNull(GraphQLString), }, }, }) @@ -129,3 +137,113 @@ export const SmsSecondFactor = new GraphQLObjectType< }, }, }) + +export const SmsSecondFactorAttributes = new GraphQLInputObjectType({ + name: "SmsSecondFactorAttributes", + fields: { + countryCode: { + type: GraphQLString, + }, + phoneNumber: { + type: GraphQLString, + }, + }, +}) + +export const AppSecondFactorAttributes = new GraphQLInputObjectType({ + name: "AppSecondFactorAttributes", + fields: { + name: { + type: GraphQLString, + }, + }, +}) + +const ErrorType = new GraphQLObjectType({ + name: "Error", + fields: () => ({ + code: { + type: new GraphQLNonNull(GraphQLString), + }, + message: { + type: new GraphQLNonNull(GraphQLString), + }, + }), +}) + +const ErrorsType = new GraphQLObjectType({ + name: "Errors", + fields: { + errors: { + type: new GraphQLList(ErrorType), + }, + }, +}) + +export const SmsSecondFactorMutationResponseOrErrorsType = new GraphQLNonNull( + new GraphQLUnionType({ + name: "SmsSecondFactorOrErrorsUnion", + types: [SmsSecondFactor, ErrorsType], + resolveType: (data) => { + if (data._id) { + return SmsSecondFactor + } + return ErrorsType + }, + }) +) + +export const AppSecondFactorMutationResponseOrErrorsType = new GraphQLNonNull( + new GraphQLUnionType({ + name: "AppSecondFactorOrErrorsUnion", + types: [AppSecondFactor, ErrorsType], + resolveType: (data) => { + if (data._id) { + return AppSecondFactor + } + return ErrorsType + }, + }) +) + +export const BackupSecondFactors = new GraphQLObjectType({ + name: "BackupSecondFactors", + fields: { + secondFactors: { + type: new GraphQLNonNull( + GraphQLList(new GraphQLNonNull(BackupSecondFactor)) + ), + }, + }, +}) + +export const BackupSecondFactorsMutationResponseOrErrorsType = new GraphQLNonNull( + new GraphQLUnionType({ + name: "BackupSecondFactorsOrErrorsUnion", + types: [BackupSecondFactors, ErrorsType], + resolveType: (data) => { + if (data.secondFactors) { + return BackupSecondFactors + } + return ErrorsType + }, + }) +) + +export const SecondFactorOrErrorsUnionType = new GraphQLNonNull( + new GraphQLUnionType({ + name: "SecondFactorOrErrorsUnion", + types: [AppSecondFactor, SmsSecondFactor, ErrorsType], + resolveType: (data) => { + if (data._id) { + switch (data.type) { + case "AppSecondFactor": + return AppSecondFactor + case "SmsSecondFactor": + return SmsSecondFactor + } + } + return ErrorsType + }, + }) +) diff --git a/src/schema/v2/schema.ts b/src/schema/v2/schema.ts index d162207b73..e7867b58db 100644 --- a/src/schema/v2/schema.ts +++ b/src/schema/v2/schema.ts @@ -246,14 +246,36 @@ import { BackupSecondFactor, AppSecondFactor, SmsSecondFactor, -} from "./me/secondFactors" + BackupSecondFactors, +} from "./me/secondFactors/secondFactors" import config from "config" +import { createSmsSecondFactorMutation } from "./me/secondFactors/mutations/createSmsSecondFactor" +import { updateSmsSecondFactorMutation } from "./me/secondFactors/mutations/updateSmsSecondFactor" +import { createAppSecondFactorMutation } from "./me/secondFactors/mutations/createAppSecondFactor" +import { updateAppSecondFactorMutation } from "./me/secondFactors/mutations/updateAppSecondFactor" +import { createBackupSecondFactorsMutation } from "./me/secondFactors/mutations/createBackupSecondFactors" +import { disableSecondFactorMutation } from "./me/secondFactors/mutations/disableSecondFactor" +import { deliverSecondFactorMutation } from "./me/secondFactors/mutations/deliverSecondFactor" +import { enableSecondFactorMutation } from "./me/secondFactors/mutations/enableSecondFactor" const useUnstitchedSecondFactorsSchema = !!config.USE_UNSTITCHED_SECOND_FACTORS_SCHEMA const secondFactorTypes = useUnstitchedSecondFactorsSchema - ? [BackupSecondFactor, AppSecondFactor, SmsSecondFactor] + ? [BackupSecondFactor, AppSecondFactor, SmsSecondFactor, BackupSecondFactors] : [] +const secondFactorMutations: any = useUnstitchedSecondFactorsSchema + ? { + createSmsSecondFactor: createSmsSecondFactorMutation, + updateSmsSecondFactor: updateSmsSecondFactorMutation, + createAppSecondFactor: createAppSecondFactorMutation, + updateAppSecondFactor: updateAppSecondFactorMutation, + createBackupSecondFactors: createBackupSecondFactorsMutation, + disableSecondFactor: disableSecondFactorMutation, + deliverSecondFactor: deliverSecondFactorMutation, + enableSecondFactor: enableSecondFactorMutation, + } + : {} + const rootFields = { // artworkVersion: ArtworkVersionResolver, // externalPartner: ExternalPartner, @@ -506,6 +528,7 @@ export default new GraphQLSchema({ updateUserInterest: updateUserInterestMutation, updateUserInterests: updateUserInterestsMutation, updateUserSaleProfile: updateUserSaleProfileMutation, + ...secondFactorMutations, }, }), query: new GraphQLObjectType({