diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index acd827f79..0bdfe5bd2 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -1484,6 +1484,25 @@ export type SearchSuccess = { pageInfo: PageInfo; }; +export type SendInstallInstructionsError = { + __typename?: 'SendInstallInstructionsError'; + errorCodes: Array; +}; + +export enum SendInstallInstructionsErrorCode { + BadRequest = 'BAD_REQUEST', + Forbidden = 'FORBIDDEN', + NotFound = 'NOT_FOUND', + Unauthorized = 'UNAUTHORIZED' +} + +export type SendInstallInstructionsResult = SendInstallInstructionsError | SendInstallInstructionsSuccess; + +export type SendInstallInstructionsSuccess = { + __typename?: 'SendInstallInstructionsSuccess'; + sent: Scalars['Boolean']; +}; + export type SetBookmarkArticleError = { __typename?: 'SetBookmarkArticleError'; errorCodes: Array; @@ -2462,6 +2481,10 @@ export type ResolversTypes = { SearchItemEdge: ResolverTypeWrapper; SearchResult: ResolversTypes['SearchError'] | ResolversTypes['SearchSuccess']; SearchSuccess: ResolverTypeWrapper; + SendInstallInstructionsError: ResolverTypeWrapper; + SendInstallInstructionsErrorCode: SendInstallInstructionsErrorCode; + SendInstallInstructionsResult: ResolversTypes['SendInstallInstructionsError'] | ResolversTypes['SendInstallInstructionsSuccess']; + SendInstallInstructionsSuccess: ResolverTypeWrapper; SetBookmarkArticleError: ResolverTypeWrapper; SetBookmarkArticleErrorCode: SetBookmarkArticleErrorCode; SetBookmarkArticleInput: SetBookmarkArticleInput; @@ -2760,6 +2783,9 @@ export type ResolversParentTypes = { SearchItemEdge: SearchItemEdge; SearchResult: ResolversParentTypes['SearchError'] | ResolversParentTypes['SearchSuccess']; SearchSuccess: SearchSuccess; + SendInstallInstructionsError: SendInstallInstructionsError; + SendInstallInstructionsResult: ResolversParentTypes['SendInstallInstructionsError'] | ResolversParentTypes['SendInstallInstructionsSuccess']; + SendInstallInstructionsSuccess: SendInstallInstructionsSuccess; SetBookmarkArticleError: SetBookmarkArticleError; SetBookmarkArticleInput: SetBookmarkArticleInput; SetBookmarkArticleResult: ResolversParentTypes['SetBookmarkArticleError'] | ResolversParentTypes['SetBookmarkArticleSuccess']; @@ -3716,6 +3742,20 @@ export type SearchSuccessResolvers; }; +export type SendInstallInstructionsErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type SendInstallInstructionsResultResolvers = { + __resolveType: TypeResolveFn<'SendInstallInstructionsError' | 'SendInstallInstructionsSuccess', ParentType, ContextType>; +}; + +export type SendInstallInstructionsSuccessResolvers = { + sent?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type SetBookmarkArticleErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -4290,6 +4330,9 @@ export type Resolvers = { SearchItemEdge?: SearchItemEdgeResolvers; SearchResult?: SearchResultResolvers; SearchSuccess?: SearchSuccessResolvers; + SendInstallInstructionsError?: SendInstallInstructionsErrorResolvers; + SendInstallInstructionsResult?: SendInstallInstructionsResultResolvers; + SendInstallInstructionsSuccess?: SendInstallInstructionsSuccessResolvers; SetBookmarkArticleError?: SetBookmarkArticleErrorResolvers; SetBookmarkArticleResult?: SetBookmarkArticleResultResolvers; SetBookmarkArticleSuccess?: SetBookmarkArticleSuccessResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index c2a0925b5..37bd033fa 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -1055,6 +1055,23 @@ type SearchSuccess { pageInfo: PageInfo! } +type SendInstallInstructionsError { + errorCodes: [SendInstallInstructionsErrorCode!]! +} + +enum SendInstallInstructionsErrorCode { + BAD_REQUEST + FORBIDDEN + NOT_FOUND + UNAUTHORIZED +} + +union SendInstallInstructionsResult = SendInstallInstructionsError | SendInstallInstructionsSuccess + +type SendInstallInstructionsSuccess { + sent: Boolean! +} + type SetBookmarkArticleError { errorCodes: [SetBookmarkArticleErrorCode!]! } diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index de13e589d..78d535387 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -57,6 +57,7 @@ import { savePageResolver, saveUrlResolver, searchResolver, + sendInstallInstructionsResolver, setBookmarkArticleResolver, setDeviceTokenResolver, setFollowResolver, @@ -176,6 +177,7 @@ export const functionResolvers = { labels: labelsResolver, search: searchResolver, subscriptions: subscriptionsResolver, + sendInstallInstructions: sendInstallInstructionsResolver, webhooks: webhooksResolver, webhook: webhookResolver, }, @@ -568,6 +570,7 @@ export const functionResolvers = { ...resultResolveTypeResolver('Subscriptions'), ...resultResolveTypeResolver('Unsubscribe'), ...resultResolveTypeResolver('UpdateLabel'), + ...resultResolveTypeResolver('SendInstallInstructions'), ...resultResolveTypeResolver('UpdatePage'), ...resultResolveTypeResolver('Subscribe'), ...resultResolveTypeResolver('AddPopularRead'), diff --git a/packages/api/src/resolvers/index.ts b/packages/api/src/resolvers/index.ts index 58434a7e4..4af611a54 100644 --- a/packages/api/src/resolvers/index.ts +++ b/packages/api/src/resolvers/index.ts @@ -11,6 +11,7 @@ export * from './report' export * from './links' export * from './newsletters' export * from './save' +export * from './send_install_instructions' export * from './reminders' export * from './user_device_tokens' export * from './labels' diff --git a/packages/api/src/resolvers/send_install_instructions/index.ts b/packages/api/src/resolvers/send_install_instructions/index.ts new file mode 100644 index 000000000..4fffe5de6 --- /dev/null +++ b/packages/api/src/resolvers/send_install_instructions/index.ts @@ -0,0 +1,43 @@ +import { + SendInstallInstructionsError, + SendInstallInstructionsErrorCode, + SendInstallInstructionsSuccess, +} from '../../generated/graphql' +import { authorized } from '../../utils/helpers' +import { sendEmail } from '../../utils/sendEmail' +import { AppDataSource } from '../../server' +import { User } from '../../entity/user' + +const INSTALL_INSTRUCTIONS_EMAIL_TEMPLATE_ID = + 'd-c576bdc3b9a849dab250655ba14c7794' + +export const sendInstallInstructionsResolver = authorized< + SendInstallInstructionsSuccess, + SendInstallInstructionsError +>(async (_parent, _args, { claims }) => { + try { + const user = await AppDataSource.getRepository(User).findOneBy({ + id: claims.uid, + }) + + if (!user) { + return { errorCodes: [SendInstallInstructionsErrorCode.Unauthorized] } + } + + const sendInstallInstructions = await sendEmail({ + from: 'msgs@omnivore.app', + templateId: INSTALL_INSTRUCTIONS_EMAIL_TEMPLATE_ID, + to: user?.email, + }) + + return { + sent: sendInstallInstructions, + } + } catch (e) { + console.log(e) + + return { + errorCodes: [SendInstallInstructionsErrorCode.BadRequest], + } + } +}) diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 1160f7314..f426f7397 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -1255,6 +1255,25 @@ const schema = gql` errorCodes: [DeleteReminderErrorCode!]! } + type SendInstallInstructionsSuccess { + sent: Boolean! + } + + enum SendInstallInstructionsErrorCode { + UNAUTHORIZED + BAD_REQUEST + NOT_FOUND + FORBIDDEN + } + + type SendInstallInstructionsError { + errorCodes: [SendInstallInstructionsErrorCode!]! + } + + union SendInstallInstructionsResult = + SendInstallInstructionsSuccess + | SendInstallInstructionsError + input SetDeviceTokenInput { id: ID token: String @@ -1776,6 +1795,7 @@ const schema = gql` labels: LabelsResult! search(after: String, first: Int, query: String): SearchResult! subscriptions(sort: SortParams): SubscriptionsResult! + sendInstallInstructions: SendInstallInstructionsResult! webhooks: WebhooksResult! webhook(id: ID!): WebhookResult! } diff --git a/packages/api/test/resolvers/send_install_instructions.test.ts b/packages/api/test/resolvers/send_install_instructions.test.ts new file mode 100644 index 000000000..6d28d7cdd --- /dev/null +++ b/packages/api/test/resolvers/send_install_instructions.test.ts @@ -0,0 +1,60 @@ +import { + createTestUser, + deleteTestUser, +} from '../db' +import { graphqlRequest, request } from '../util' +import 'mocha' + +describe('Send Install Instructions API', () => { + const username = 'fakeUser' + + let authToken: string + + before(async () => { + // create test user and login + const user = await createTestUser(username) + const res = await request + .post('/local/debug/fake-user-login') + .send({ fakeEmail: user.email }) + + authToken = res.body.authToken + }) + + after(async () => { + // clean up + await deleteTestUser(username) + }) + + describe('Send install instructions', () => { + const query = ` + query SendInstallInstructions { + sendInstallInstructions { + ... on SendInstallInstructionsSuccess { + sent + } + } + + sendInstallInstructions { + ... on SendInstallInstructionsError { + errorCodes + } + } + } + ` + + it('responds status code 400 when invalid query', async () => { + const invalidQuery = ` + query { + sendInstallInstructions { + } + } + ` + return graphqlRequest(invalidQuery, authToken).expect(400) + }) + + it('responds status code 500 when invalid user', async () => { + const invalidAuthToken = 'Fake token' + return graphqlRequest(query, invalidAuthToken).expect(500) + }) + }) +})