diff --git a/.changeset/itchy-owls-obey.md b/.changeset/itchy-owls-obey.md new file mode 100644 index 00000000..ba745794 --- /dev/null +++ b/.changeset/itchy-owls-obey.md @@ -0,0 +1,11 @@ +--- +"@zemble/auth-otp": patch +"@zemble/auth": patch +"@zemble/cms-users": patch +"@zemble/cms": patch +"cms-ui": patch +"minimal": patch +"supplement-stack": patch +--- + +Login with phone number diff --git a/apps/cms-ui/app/(auth)/login.tsx b/apps/cms-ui/app/(auth)/login.tsx index 6f52c790..d4bbd960 100644 --- a/apps/cms-ui/app/(auth)/login.tsx +++ b/apps/cms-ui/app/(auth)/login.tsx @@ -18,7 +18,7 @@ type TextInputHandles = Pick { const doConfirm = useCallback(async () => { const { data } = await loginConfirm({ email, code }) - if (data?.loginConfirm.__typename === 'LoginConfirmSuccessfulResponse') { - setToken(data.loginConfirm.bearerToken) + if (data?.loginConfirmWithEmail.__typename === 'LoginConfirmSuccessfulResponse') { + setToken(data.loginConfirmWithEmail.bearerToken) } }, [ email, code, loginConfirm, setToken, diff --git a/apps/minimal/.env.dev b/apps/minimal/.env.dev new file mode 100644 index 00000000..134b71b3 --- /dev/null +++ b/apps/minimal/.env.dev @@ -0,0 +1,67 @@ + +PUBLIC_KEY='-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArRjgBxq2WSsxHvXeM0Gu +fa1s1Ovoo3UQfd239dO+vxc8H4FbO60KYkVwSPwTfqOvre5pp5XkyFFhHonNNrQa +qAzPCihYhIwvL0H7CvOaQqDCDGt5+UZnQ1gHxng9zTDOP3NKtmZ6d9VDHydK+5tW +3sObf2aLXA19W1r4s++SzWtr1zkHs+NJ2L8m0Jm/mDu45n1pz4eKZK43iTwUcpRz +SPfreK8lzKulhP+Ehr+1o6Px3y5C4uHdl0FN6B6bdbR/OfQuCDQ+KBIeW13epEep +0W675WIUF7c7lCyzstskRPkB9SITYTjvzEesWarQo9C9vr22AGCF70JJbuGoDAk2 +TpPEA1S4bZWZxwdTTV6Ys9iQd4dSWh39G+rnt7I2QMJhHmGnYA/PSDGxyDd9y5ub +prKGX5UERdGAPEZxgJjCRFhOZgCsYXMewg246oUpMe8Efvi67DqZqPHJmJK2++Cz +nEhrZXqEvSSpVvnP89RXutLCvOdFeWxx4NlPpjwI0Whs+JPrpFuqXKhRX7cbk33X +zD79jNdVXZEWDCwOkENeDqfPM5KiQD3y6geDAFghBge9Xwb9FMPCJ9GV2Zt2cbFL +PmS5NlwkjP5O9jHswPFZdsPT5oud/zrNB5OYf2J1qyUl0y6tu7ryH697p9PJECXP +pduX76EzFL7Qe9r9jlUNCm8CAwEAAQ== +-----END PUBLIC KEY-----' +PRIVATE_KEY='-----BEGIN PRIVATE KEY----- +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 +DmxKOUbG7CHn8nB21sC7cvKKEIxoCg== +-----END PRIVATE KEY-----' \ No newline at end of file diff --git a/apps/minimal/.env.test b/apps/minimal/.env.test new file mode 100644 index 00000000..134b71b3 --- /dev/null +++ b/apps/minimal/.env.test @@ -0,0 +1,67 @@ + +PUBLIC_KEY='-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArRjgBxq2WSsxHvXeM0Gu +fa1s1Ovoo3UQfd239dO+vxc8H4FbO60KYkVwSPwTfqOvre5pp5XkyFFhHonNNrQa +qAzPCihYhIwvL0H7CvOaQqDCDGt5+UZnQ1gHxng9zTDOP3NKtmZ6d9VDHydK+5tW +3sObf2aLXA19W1r4s++SzWtr1zkHs+NJ2L8m0Jm/mDu45n1pz4eKZK43iTwUcpRz +SPfreK8lzKulhP+Ehr+1o6Px3y5C4uHdl0FN6B6bdbR/OfQuCDQ+KBIeW13epEep +0W675WIUF7c7lCyzstskRPkB9SITYTjvzEesWarQo9C9vr22AGCF70JJbuGoDAk2 +TpPEA1S4bZWZxwdTTV6Ys9iQd4dSWh39G+rnt7I2QMJhHmGnYA/PSDGxyDd9y5ub +prKGX5UERdGAPEZxgJjCRFhOZgCsYXMewg246oUpMe8Efvi67DqZqPHJmJK2++Cz +nEhrZXqEvSSpVvnP89RXutLCvOdFeWxx4NlPpjwI0Whs+JPrpFuqXKhRX7cbk33X +zD79jNdVXZEWDCwOkENeDqfPM5KiQD3y6geDAFghBge9Xwb9FMPCJ9GV2Zt2cbFL +PmS5NlwkjP5O9jHswPFZdsPT5oud/zrNB5OYf2J1qyUl0y6tu7ryH697p9PJECXP +pduX76EzFL7Qe9r9jlUNCm8CAwEAAQ== +-----END PUBLIC KEY-----' +PRIVATE_KEY='-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCtGOAHGrZZKzEe +9d4zQa59rWzU6+ijdRB93bf1076/FzwfgVs7rQpiRXBI/BN+o6+t7mmnleTIUWEe +ic02tBqoDM8KKFiEjC8vQfsK85pCoMIMa3n5RmdDWAfGeD3NMM4/c0q2Znp31UMf +J0r7m1bew5t/ZotcDX1bWviz75LNa2vXOQez40nYvybQmb+YO7jmfWnPh4pkrjeJ +PBRylHNI9+t4ryXMq6WE/4SGv7Wjo/HfLkLi4d2XQU3oHpt1tH859C4IND4oEh5b +Xd6kR6nRbrvlYhQXtzuULLOy2yRE+QH1IhNhOO/MR6xZqtCj0L2+vbYAYIXvQklu +4agMCTZOk8QDVLhtlZnHB1NNXpiz2JB3h1JaHf0b6ue3sjZAwmEeYadgD89IMbHI +N33Lm5umsoZflQRF0YA8RnGAmMJEWE5mAKxhcx7CDbjqhSkx7wR++LrsOpmo8cmY +krb74LOcSGtleoS9JKlW+c/z1Fe60sK850V5bHHg2U+mPAjRaGz4k+ukW6pcqFFf +txuTfdfMPv2M11VdkRYMLA6QQ14Op88zkqJAPfLqB4MAWCEGB71fBv0Uw8In0ZXZ +m3ZxsUs+ZLk2XCSM/k72MezA8Vl2w9Pmi53/Os0Hk5h/YnWrJSXTLq27uvIfr3un +08kQJc+l25fvoTMUvtB72v2OVQ0KbwIDAQABAoICAElLLSEU+H1NQqFfblS5zQ6+ +LkUCFyGPYgpJpalbnXsAdZ7JgT4FiU4PJPpicka/PJYjS22AlL0VaFAr/U7aeCcM +NWaRqtmFGj2ibJA4kesAcuobYCxi2Tq0vaYWJ+UIc2x+fvY48kZncOJyGtfq8J+m +p+MMIpo5xAi4vnJCUSDWCXW6Wj6bURuL5P2S/IOTVjmShQkw1TLIag+b9vSFU0Nk +ZrpVP2YokA9+ujjsWBcQRkxAJGY5SUDQkBJnU2BFq1F4yT0EEgWZlm0NlEZvacKa +Zsk7/FIGbh3Rx81F6VejMGcBpL8YAsG2342gPHQA4Tcw0S3bsRqcMMfYmPVvuSRc +2g1gRzdYgNBX4AYzqgozOEIue0Rifs+aXzLxfQBxxgpzfuZl+41xK72CJV7oWeJ3 +UAvKljZ/yxi7k+pRBaRgUUYKPITvO8s7uv8QBpuLWmZBaW4TkJn9XjR8iFGIz6WI +LDWGYhpJowGfL5aUacP4oZtcHaIjlxfTAMDV36f6y/fug1oZ4oeXx9CnASBiSp5G +5f/zAm0e2KR1VLReleGqyfkB8CeefRjr/keO9uZ2uB9GuXY/noMTYpRxiDiiSQ2H +fA+jUBa1n/gIrk/QzkU2a3e53KJoGxKpXHtLhfWfTm4hBkWpnVwHSESVZmoFu2Pn +PhpsgJQoizrA8sYKwiDRAoIBAQDuaWoXhcQKFE0HB9+DiUS5hlkruTAXccX49qhb +D7ATsHoZlB11JFUob2j9cKcgOYgJtG9zYrEyMvPsqVMQDLeaI14YeDRnkYeuc0KI +TsKQpK6DXweMJ5CKhMpUr0VCOEYBn+Hg2eCnnKU2SFFZOTgCwPuN8K3QQ+RxamfI +K7NeB0E0Tl/wHjMenh5HtOzrFCTvK87qx3qrSg1K7zldr4xIySae1w8oUuXQxrJx +ONbSKlfdyqwQrqHjgyIGZVq8hDRuU1mgX6IZuCXNQSMIZRnM3vHLM2Q1HhRCWPse +o6sSfUqcj2tF6X1p1qpzs4Yoz4Yg9mxvveFjRfbiL7j+PezrAoIBAQC53fH7kG5S +V4urmotACZFtbmb9iwEyaI5DbgyyF+qHZ46y5BCdH0QssRGDt0OvKphGFkSvFzqj +ram1H8gVtTkk7AA3vRFHi7quVIUbX5PTvPStrDHCG4xiGdFC6X2rqbfhj0tpxgOf +0RZ7p3bA29PX3uJN09MIyTvDiJZYQeBpdFv3LEVi9lS6P7JJ/Eoa4eg03FMa5fey +sPjdYnu/0axp4DXpw6+9qkfE8MgRw1zSgioPtcRoH57InzNPWtWnxKkXuSMgshkP +AB0o2c/rOSC3s9MarDZ/UEYsocJefPObbliRVvzb9ZinSBMTfLNof4vOUUZIXTDD +oC/Nj3+kHWeNAoIBAQDIhh1Xn4jduWl4KmUSq1gEez1xCxeqAGW1GzFLNgyw4tQv +w2Zrap29nsc9s4y+C+Wh1AORl0bWpAiJ23kjnK6olGrGRgQ9wCfQ0Nz67NO0+O5X +TIx6k6R8/Wd4htrH8bDgGOvRRTAxig1jdLwHQZscpdhu8Coha5ku04Jq2GbLBWsQ +ZYNH4P66F9/8C+7hYlYWXFBzJEjG3UgE6OTlKYvihF1ZaCU1k1P8n8LifB4jMr4o +rBjBB0DerSLnIkSJHuFCkhRmUUl0PHB/DPQ8UVy79iCE+rqj+qKrk0/T23E94efp +eq+NY++6H3XKTwreNH3qJ5nY+moiJ2yZne4I2Wj1AoIBABJjlxFglMmox7TOsYBA +chb1mVL0ccXe+lRJixADtbx3znJ8hfhFo6UYT7dMZnHqMO6ePWVlUvPmtqeo4U7k +gybYAp2BIziE5o9g5Vpz9lg4laypILMnrpf2HfIUz84wGHVEjB2G7czeDu9k/ibO +mQL+lj1E+9gesL8DwHwy6FUGoiKuSp8j5/YVXzpar6pBN4wjUEWVAxC22ahtmfi8 +ceZ3vF3Icpa9RiSB/glj3sclAaxUO35hwn/u4FC1l/XlSyWBt6wAcuI9DKWCB0Qq +fsYuEHDPIQUvmAHKb0SR8Dgsjq6ygInynovOYbTH3ag1FkvvVpjKP4zbSF96FxLR +pfECggEAezK+f8cSiG1kFFJgwKQ095lsNEgRFDqIVE2lwrc2amJS/now7Qiw2+Fd +SILIxE2DLWTYNYEKAp4KIhjmA/0Jl9lBxEXrSqUahVT6D+tqklOl51Z1VAssNzo1 +l+vx1SBGeXudBa4am3OvHlS4ICpB3g5mKFv57h0tOHfYmZlMSONQiqxQKGgE6L7A +AKXlUW0XRlJ1KXQTPtjSCFJEINimeVcJw144gABhZADscNrme9MwgD4BuKNVL1w9 +535AoMfJOVAZ6SyDOf9UkWI9lZ9gOMTp4XI7FAMfkZlQ9LU3S7oJbaYZaGdiN7sQ +DmxKOUbG7CHn8nB21sC7cvKKEIxoCg== +-----END PRIVATE KEY-----' \ No newline at end of file diff --git a/apps/minimal/.gitignore b/apps/minimal/.gitignore new file mode 100644 index 00000000..e6905a23 --- /dev/null +++ b/apps/minimal/.gitignore @@ -0,0 +1 @@ +.env* \ No newline at end of file diff --git a/apps/minimal/app.ts b/apps/minimal/app.ts index 75592ab3..3d3720f0 100644 --- a/apps/minimal/app.ts +++ b/apps/minimal/app.ts @@ -1,5 +1,6 @@ import Bull from '@zemble/bull' import { createApp } from '@zemble/core' +import Resend from '@zemble/email-resend' import GraphQL from '@zemble/graphql' import GraphQLLogger from '@zemble/logger-graphql' import Migrations from '@zemble/migrations' @@ -26,5 +27,9 @@ export default createApp({ Migrations.configure({ createAdapter: () => dryrunAdapter, }), + Resend.configure({ + RESEND_API_KEY: process.env['RESEND_API_KEY'], + disable: false, + }), ], }) diff --git a/apps/minimal/graphql/schema.graphql b/apps/minimal/graphql/schema.graphql index aea15a76..3fbc1344 100644 --- a/apps/minimal/graphql/schema.graphql +++ b/apps/minimal/graphql/schema.graphql @@ -4,6 +4,11 @@ type Query { type Mutation { randomNumber: Int! + # sendSms( + # from: String!, + # to: String!, + # message: String!, + # ): Boolean! } type Subscription { diff --git a/apps/minimal/package.json b/apps/minimal/package.json index 26a86216..693c4848 100644 --- a/apps/minimal/package.json +++ b/apps/minimal/package.json @@ -36,15 +36,19 @@ }, "license": "ISC", "dependencies": { + "@zemble/auth": "workspace:*", + "@zemble/auth-otp": "workspace:*", + "@zemble/bull": "workspace:*", + "@zemble/bun": "workspace:*", "@zemble/core": "workspace:*", + "@zemble/email-resend": "workspace:*", "@zemble/graphql": "workspace:*", + "@zemble/logger-graphql": "workspace:*", "@zemble/migrations": "workspace:*", - "@zemble/bun": "workspace:*", - "@zemble/auth": "workspace:*", - "@zemble/routes": "workspace:*", "@zemble/pino": "workspace:*", - "@zemble/bull": "workspace:*", - "@zemble/logger-graphql": "workspace:*" + "@zemble/routes": "workspace:*", + "@zemble/sms-46elks": "workspace:*", + "@zemble/sms-twilio": "workspace:*" }, "devDependencies": { "@tsconfig/node20": "^20.1.4", diff --git a/apps/supplement-stack/app.ts b/apps/supplement-stack/app.ts index 45a0d0ea..9807a457 100644 --- a/apps/supplement-stack/app.ts +++ b/apps/supplement-stack/app.ts @@ -26,7 +26,7 @@ void bunRunner({ }, }), AuthOTP.configure({ - from: { email: 'robert@herber.me' }, + fromEmail: { email: 'robert@herber.me' }, generateTokenContents: async ({ email }) => { const user = await Users.findOneAndUpdate({ email }, { $set: { diff --git a/apps/supplement-stack/graphql/schema.generated.ts b/apps/supplement-stack/graphql/schema.generated.ts index e2a090c3..2bfd50d3 100644 --- a/apps/supplement-stack/graphql/schema.generated.ts +++ b/apps/supplement-stack/graphql/schema.generated.ts @@ -19,20 +19,9 @@ export type Scalars = { Int: { input: number; output: number; } Float: { input: number; output: number; } DateTime: { input: string; output: string; } - JSONObject: { input: any; output: any; } ObjectId: { input: string; output: string; } }; -export type AuthOr = { - readonly includes?: InputMaybe; - readonly match?: InputMaybe; -}; - -export type CodeNotValidError = Error & { - readonly __typename?: 'CodeNotValidError'; - readonly message: Scalars['String']['output']; -}; - export type Eatable = { readonly _id: Scalars['ObjectId']['output']; readonly createdAt: Scalars['DateTime']['output']; @@ -55,15 +44,6 @@ export type EatableProportionInput = { readonly proportion: Scalars['Float']['input']; }; -export type EmailNotValidError = Error & { - readonly __typename?: 'EmailNotValidError'; - readonly message: Scalars['String']['output']; -}; - -export type Error = { - readonly message: Scalars['String']['output']; -}; - export type Food = Eatable & { readonly __typename?: 'Food'; readonly _id: Scalars['ObjectId']['output']; @@ -100,38 +80,11 @@ export enum IntakeTime { Wakeup = 'WAKEUP' } -export type LoginConfirmResponse = CodeNotValidError | EmailNotValidError | LoginConfirmSuccessfulResponse | LoginFailedError; - -export type LoginConfirmSuccessfulResponse = { - readonly __typename?: 'LoginConfirmSuccessfulResponse'; - readonly accessToken: Scalars['String']['output']; -}; - -export type LoginFailedError = Error & { - readonly __typename?: 'LoginFailedError'; - readonly message: Scalars['String']['output']; -}; - -export type LoginRequestResponse = EmailNotValidError | LoginRequestSuccessResponse; - -export type LoginRequestSuccessResponse = { - readonly __typename?: 'LoginRequestSuccessResponse'; - readonly success: Scalars['Boolean']['output']; -}; - -export type LoginResponse = { - readonly __typename?: 'LoginResponse'; - readonly token: Scalars['String']['output']; -}; - export type Mutation = { readonly __typename?: 'Mutation'; readonly addFood: Food; readonly addIngredient: Ingredient; readonly addSupplement: SupplementIntake; - readonly loginConfirm: LoginConfirmResponse; - readonly loginRequest: LoginRequestResponse; - readonly logout: LoginRequestSuccessResponse; }; @@ -160,17 +113,6 @@ export type MutationAddSupplementArgs = { supplementId?: InputMaybe; }; - -export type MutationLoginConfirmArgs = { - code: Scalars['String']['input']; - email: Scalars['String']['input']; -}; - - -export type MutationLoginRequestArgs = { - email: Scalars['String']['input']; -}; - export type NutrientQuantity = { readonly __typename?: 'NutrientQuantity'; readonly amountInGrams: Scalars['Float']['output']; @@ -428,41 +370,20 @@ export type DirectiveResolverFn TResult | Promise; -/** Mapping of union types */ -export type ResolversUnionTypes> = ResolversObject<{ - LoginConfirmResponse: ( CodeNotValidError ) | ( EmailNotValidError ) | ( LoginConfirmSuccessfulResponse ) | ( LoginFailedError ); - LoginRequestResponse: ( EmailNotValidError ) | ( LoginRequestSuccessResponse ); -}>; -/** Mapping of interface types */ -export type ResolversInterfaceTypes> = ResolversObject<{ - Eatable: ( EatableDbType ) | ( EatableDbType ); - Error: ( CodeNotValidError ) | ( EmailNotValidError ) | ( LoginFailedError ); -}>; /** Mapping between all available schema types and the resolvers types */ export type ResolversTypes = ResolversObject<{ - AuthOr: AuthOr; Boolean: ResolverTypeWrapper; - CodeNotValidError: ResolverTypeWrapper; DateTime: ResolverTypeWrapper; Eatable: ResolverTypeWrapper; EatableProportion: ResolverTypeWrapper & { eatable: ResolversTypes['Eatable'] }>; EatableProportionInput: EatableProportionInput; - EmailNotValidError: ResolverTypeWrapper; - Error: ResolverTypeWrapper['Error']>; Float: ResolverTypeWrapper; Food: ResolverTypeWrapper; Image: ResolverTypeWrapper; Ingredient: ResolverTypeWrapper; IntakeTime: IntakeTime; - JSONObject: ResolverTypeWrapper; - LoginConfirmResponse: ResolverTypeWrapper['LoginConfirmResponse']>; - LoginConfirmSuccessfulResponse: ResolverTypeWrapper; - LoginFailedError: ResolverTypeWrapper; - LoginRequestResponse: ResolverTypeWrapper['LoginRequestResponse']>; - LoginRequestSuccessResponse: ResolverTypeWrapper; - LoginResponse: ResolverTypeWrapper; Mutation: ResolverTypeWrapper<{}>; NutrientQuantity: ResolverTypeWrapper; NutrientQuantityInput: NutrientQuantityInput; @@ -480,26 +401,15 @@ export type ResolversTypes = ResolversObject<{ /** Mapping between all available schema types and the resolvers parents */ export type ResolversParentTypes = ResolversObject<{ - AuthOr: AuthOr; Boolean: Scalars['Boolean']['output']; - CodeNotValidError: CodeNotValidError; DateTime: Scalars['DateTime']['output']; Eatable: EatableDbType; EatableProportion: Omit & { eatable: ResolversParentTypes['Eatable'] }; EatableProportionInput: EatableProportionInput; - EmailNotValidError: EmailNotValidError; - Error: ResolversInterfaceTypes['Error']; Float: Scalars['Float']['output']; Food: EatableDbType; Image: Image; Ingredient: EatableDbType; - JSONObject: Scalars['JSONObject']['output']; - LoginConfirmResponse: ResolversUnionTypes['LoginConfirmResponse']; - LoginConfirmSuccessfulResponse: LoginConfirmSuccessfulResponse; - LoginFailedError: LoginFailedError; - LoginRequestResponse: ResolversUnionTypes['LoginRequestResponse']; - LoginRequestSuccessResponse: LoginRequestSuccessResponse; - LoginResponse: LoginResponse; Mutation: {}; NutrientQuantity: NutrientQuantity; NutrientQuantityInput: NutrientQuantityInput; @@ -512,20 +422,6 @@ export type ResolversParentTypes = ResolversObject<{ User: User; }>; -export type AuthDirectiveArgs = { - includes?: Maybe; - match?: Maybe; - or?: Maybe>; - skip?: Maybe; -}; - -export type AuthDirectiveResolver = DirectiveResolverFn; - -export type CodeNotValidErrorResolvers = ResolversObject<{ - message?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig { name: 'DateTime'; } @@ -548,16 +444,6 @@ export type EatableProportionResolvers; }>; -export type EmailNotValidErrorResolvers = ResolversObject<{ - message?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ErrorResolvers = ResolversObject<{ - __resolveType: TypeResolveFn<'CodeNotValidError' | 'EmailNotValidError' | 'LoginFailedError', ParentType, ContextType>; - message?: Resolver>; -}>; - export type FoodResolvers = ResolversObject<{ _id?: Resolver; createdAt?: Resolver; @@ -586,45 +472,10 @@ export type IngredientResolvers; }>; -export interface JsonObjectScalarConfig extends GraphQLScalarTypeConfig { - name: 'JSONObject'; -} - -export type LoginConfirmResponseResolvers = ResolversObject<{ - __resolveType: TypeResolveFn<'CodeNotValidError' | 'EmailNotValidError' | 'LoginConfirmSuccessfulResponse' | 'LoginFailedError', ParentType, ContextType>; -}>; - -export type LoginConfirmSuccessfulResponseResolvers = ResolversObject<{ - accessToken?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type LoginFailedErrorResolvers = ResolversObject<{ - message?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type LoginRequestResponseResolvers = ResolversObject<{ - __resolveType: TypeResolveFn<'EmailNotValidError' | 'LoginRequestSuccessResponse', ParentType, ContextType>; -}>; - -export type LoginRequestSuccessResponseResolvers = ResolversObject<{ - success?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type LoginResponseResolvers = ResolversObject<{ - token?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - export type MutationResolvers = ResolversObject<{ addFood?: Resolver>; addIngredient?: Resolver>; addSupplement?: Resolver>; - loginConfirm?: Resolver, RequireFields>; - loginRequest?: Resolver, RequireFields>; - logout?: Resolver; }>; export type NutrientQuantityResolvers = ResolversObject<{ @@ -664,22 +515,12 @@ export type UserResolvers; export type Resolvers = ResolversObject<{ - CodeNotValidError?: CodeNotValidErrorResolvers; DateTime?: GraphQLScalarType; Eatable?: EatableResolvers; EatableProportion?: EatableProportionResolvers; - EmailNotValidError?: EmailNotValidErrorResolvers; - Error?: ErrorResolvers; Food?: FoodResolvers; Image?: ImageResolvers; Ingredient?: IngredientResolvers; - JSONObject?: GraphQLScalarType; - LoginConfirmResponse?: LoginConfirmResponseResolvers; - LoginConfirmSuccessfulResponse?: LoginConfirmSuccessfulResponseResolvers; - LoginFailedError?: LoginFailedErrorResolvers; - LoginRequestResponse?: LoginRequestResponseResolvers; - LoginRequestSuccessResponse?: LoginRequestSuccessResponseResolvers; - LoginResponse?: LoginResponseResolvers; Mutation?: MutationResolvers; NutrientQuantity?: NutrientQuantityResolvers; ObjectId?: GraphQLScalarType; @@ -689,9 +530,6 @@ export type Resolvers = ResolversObject<{ User?: UserResolvers; }>; -export type DirectiveResolvers = ResolversObject<{ - auth?: AuthDirectiveResolver; -}>; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { @@ -701,20 +539,9 @@ export type Scalars = { Int: { input: number; output: number; } Float: { input: number; output: number; } DateTime: { input: string; output: string; } - JSONObject: { input: any; output: any; } ObjectId: { input: string; output: string; } }; -export type AuthOr = { - readonly includes?: InputMaybe; - readonly match?: InputMaybe; -}; - -export type CodeNotValidError = Error & { - readonly __typename?: 'CodeNotValidError'; - readonly message: Scalars['String']['output']; -}; - export type Eatable = { readonly _id: Scalars['ObjectId']['output']; readonly createdAt: Scalars['DateTime']['output']; @@ -737,15 +564,6 @@ export type EatableProportionInput = { readonly proportion: Scalars['Float']['input']; }; -export type EmailNotValidError = Error & { - readonly __typename?: 'EmailNotValidError'; - readonly message: Scalars['String']['output']; -}; - -export type Error = { - readonly message: Scalars['String']['output']; -}; - export type Food = Eatable & { readonly __typename?: 'Food'; readonly _id: Scalars['ObjectId']['output']; @@ -782,38 +600,11 @@ export enum IntakeTime { Wakeup = 'WAKEUP' } -export type LoginConfirmResponse = CodeNotValidError | EmailNotValidError | LoginConfirmSuccessfulResponse | LoginFailedError; - -export type LoginConfirmSuccessfulResponse = { - readonly __typename?: 'LoginConfirmSuccessfulResponse'; - readonly accessToken: Scalars['String']['output']; -}; - -export type LoginFailedError = Error & { - readonly __typename?: 'LoginFailedError'; - readonly message: Scalars['String']['output']; -}; - -export type LoginRequestResponse = EmailNotValidError | LoginRequestSuccessResponse; - -export type LoginRequestSuccessResponse = { - readonly __typename?: 'LoginRequestSuccessResponse'; - readonly success: Scalars['Boolean']['output']; -}; - -export type LoginResponse = { - readonly __typename?: 'LoginResponse'; - readonly token: Scalars['String']['output']; -}; - export type Mutation = { readonly __typename?: 'Mutation'; readonly addFood: Food; readonly addIngredient: Ingredient; readonly addSupplement: SupplementIntake; - readonly loginConfirm: LoginConfirmResponse; - readonly loginRequest: LoginRequestResponse; - readonly logout: LoginRequestSuccessResponse; }; @@ -842,17 +633,6 @@ export type MutationAddSupplementArgs = { supplementId?: InputMaybe; }; - -export type MutationLoginConfirmArgs = { - code: Scalars['String']['input']; - email: Scalars['String']['input']; -}; - - -export type MutationLoginRequestArgs = { - email: Scalars['String']['input']; -}; - export type NutrientQuantity = { readonly __typename?: 'NutrientQuantity'; readonly amountInGrams: Scalars['Float']['output']; @@ -1110,41 +890,20 @@ export type DirectiveResolverFn TResult | Promise; -/** Mapping of union types */ -export type ResolversUnionTypes> = ResolversObject<{ - LoginConfirmResponse: ( CodeNotValidError ) | ( EmailNotValidError ) | ( LoginConfirmSuccessfulResponse ) | ( LoginFailedError ); - LoginRequestResponse: ( EmailNotValidError ) | ( LoginRequestSuccessResponse ); -}>; -/** Mapping of interface types */ -export type ResolversInterfaceTypes> = ResolversObject<{ - Eatable: ( EatableDbType ) | ( EatableDbType ); - Error: ( CodeNotValidError ) | ( EmailNotValidError ) | ( LoginFailedError ); -}>; /** Mapping between all available schema types and the resolvers types */ export type ResolversTypes = ResolversObject<{ - AuthOr: AuthOr; Boolean: ResolverTypeWrapper; - CodeNotValidError: ResolverTypeWrapper; DateTime: ResolverTypeWrapper; Eatable: ResolverTypeWrapper; EatableProportion: ResolverTypeWrapper & { eatable: ResolversTypes['Eatable'] }>; EatableProportionInput: EatableProportionInput; - EmailNotValidError: ResolverTypeWrapper; - Error: ResolverTypeWrapper['Error']>; Float: ResolverTypeWrapper; Food: ResolverTypeWrapper; Image: ResolverTypeWrapper; Ingredient: ResolverTypeWrapper; IntakeTime: IntakeTime; - JSONObject: ResolverTypeWrapper; - LoginConfirmResponse: ResolverTypeWrapper['LoginConfirmResponse']>; - LoginConfirmSuccessfulResponse: ResolverTypeWrapper; - LoginFailedError: ResolverTypeWrapper; - LoginRequestResponse: ResolverTypeWrapper['LoginRequestResponse']>; - LoginRequestSuccessResponse: ResolverTypeWrapper; - LoginResponse: ResolverTypeWrapper; Mutation: ResolverTypeWrapper<{}>; NutrientQuantity: ResolverTypeWrapper; NutrientQuantityInput: NutrientQuantityInput; @@ -1162,26 +921,15 @@ export type ResolversTypes = ResolversObject<{ /** Mapping between all available schema types and the resolvers parents */ export type ResolversParentTypes = ResolversObject<{ - AuthOr: AuthOr; Boolean: Scalars['Boolean']['output']; - CodeNotValidError: CodeNotValidError; DateTime: Scalars['DateTime']['output']; Eatable: EatableDbType; EatableProportion: Omit & { eatable: ResolversParentTypes['Eatable'] }; EatableProportionInput: EatableProportionInput; - EmailNotValidError: EmailNotValidError; - Error: ResolversInterfaceTypes['Error']; Float: Scalars['Float']['output']; Food: EatableDbType; Image: Image; Ingredient: EatableDbType; - JSONObject: Scalars['JSONObject']['output']; - LoginConfirmResponse: ResolversUnionTypes['LoginConfirmResponse']; - LoginConfirmSuccessfulResponse: LoginConfirmSuccessfulResponse; - LoginFailedError: LoginFailedError; - LoginRequestResponse: ResolversUnionTypes['LoginRequestResponse']; - LoginRequestSuccessResponse: LoginRequestSuccessResponse; - LoginResponse: LoginResponse; Mutation: {}; NutrientQuantity: NutrientQuantity; NutrientQuantityInput: NutrientQuantityInput; @@ -1194,20 +942,6 @@ export type ResolversParentTypes = ResolversObject<{ User: User; }>; -export type AuthDirectiveArgs = { - includes?: Maybe; - match?: Maybe; - or?: Maybe>; - skip?: Maybe; -}; - -export type AuthDirectiveResolver = DirectiveResolverFn; - -export type CodeNotValidErrorResolvers = ResolversObject<{ - message?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig { name: 'DateTime'; } @@ -1230,16 +964,6 @@ export type EatableProportionResolvers; }>; -export type EmailNotValidErrorResolvers = ResolversObject<{ - message?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type ErrorResolvers = ResolversObject<{ - __resolveType: TypeResolveFn<'CodeNotValidError' | 'EmailNotValidError' | 'LoginFailedError', ParentType, ContextType>; - message?: Resolver>; -}>; - export type FoodResolvers = ResolversObject<{ _id?: Resolver; createdAt?: Resolver; @@ -1268,45 +992,10 @@ export type IngredientResolvers; }>; -export interface JsonObjectScalarConfig extends GraphQLScalarTypeConfig { - name: 'JSONObject'; -} - -export type LoginConfirmResponseResolvers = ResolversObject<{ - __resolveType: TypeResolveFn<'CodeNotValidError' | 'EmailNotValidError' | 'LoginConfirmSuccessfulResponse' | 'LoginFailedError', ParentType, ContextType>; -}>; - -export type LoginConfirmSuccessfulResponseResolvers = ResolversObject<{ - accessToken?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type LoginFailedErrorResolvers = ResolversObject<{ - message?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type LoginRequestResponseResolvers = ResolversObject<{ - __resolveType: TypeResolveFn<'EmailNotValidError' | 'LoginRequestSuccessResponse', ParentType, ContextType>; -}>; - -export type LoginRequestSuccessResponseResolvers = ResolversObject<{ - success?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type LoginResponseResolvers = ResolversObject<{ - token?: Resolver>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - export type MutationResolvers = ResolversObject<{ addFood?: Resolver>; addIngredient?: Resolver>; addSupplement?: Resolver>; - loginConfirm?: Resolver, RequireFields>; - loginRequest?: Resolver, RequireFields>; - logout?: Resolver; }>; export type NutrientQuantityResolvers = ResolversObject<{ @@ -1346,22 +1035,12 @@ export type UserResolvers; export type Resolvers = ResolversObject<{ - CodeNotValidError?: CodeNotValidErrorResolvers; DateTime?: GraphQLScalarType; Eatable?: EatableResolvers; EatableProportion?: EatableProportionResolvers; - EmailNotValidError?: EmailNotValidErrorResolvers; - Error?: ErrorResolvers; Food?: FoodResolvers; Image?: ImageResolvers; Ingredient?: IngredientResolvers; - JSONObject?: GraphQLScalarType; - LoginConfirmResponse?: LoginConfirmResponseResolvers; - LoginConfirmSuccessfulResponse?: LoginConfirmSuccessfulResponseResolvers; - LoginFailedError?: LoginFailedErrorResolvers; - LoginRequestResponse?: LoginRequestResponseResolvers; - LoginRequestSuccessResponse?: LoginRequestSuccessResponseResolvers; - LoginResponse?: LoginResponseResolvers; Mutation?: MutationResolvers; NutrientQuantity?: NutrientQuantityResolvers; ObjectId?: GraphQLScalarType; @@ -1371,6 +1050,3 @@ export type Resolvers = ResolversObject<{ User?: UserResolvers; }>; -export type DirectiveResolvers = ResolversObject<{ - auth?: AuthDirectiveResolver; -}>; diff --git a/bun.lockb b/bun.lockb index 15f4fba7..d50154de 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/auth-otp/graphql/Mutation/loginConfirm.ts b/packages/auth-otp/graphql/Mutation/loginConfirm.ts deleted file mode 100644 index 88a74c3b..00000000 --- a/packages/auth-otp/graphql/Mutation/loginConfirm.ts +++ /dev/null @@ -1,57 +0,0 @@ -import Auth from '@zemble/auth' -import { generateRefreshToken } from '@zemble/auth/utils/generateRefreshToken' -import { setTokenCookies } from '@zemble/auth/utils/setBearerTokenCookie' -import { signJwt } from '@zemble/auth/utils/signJwt' - -import { loginRequestKeyValue } from '../../clients/loginRequestKeyValue' -import plugin from '../../plugin' -import { isValidEmail } from '../../utils/isValidEmail' - -import type { - MutationResolvers, -} from '../schema.generated' - -export const loginConfirm: NonNullable = async (_, { - email: emailIn, code, -}, { honoContext }) => { - const email = emailIn.toLowerCase().trim() - if (!isValidEmail(email)) { - return { __typename: 'EmailNotValidError', message: 'Email not valid' } - } - - if (code.length !== 6) { - return { __typename: 'CodeNotValidError', message: 'Code should be 6 characters' } - } - - const entry = await loginRequestKeyValue().get(email.toLowerCase()) - - if (!entry) { - return { __typename: 'CodeNotValidError', message: 'Must loginRequest code first, it might have expired' } - } - - if (entry?.twoFactorCode !== code) { - return { __typename: 'CodeNotValidError', message: 'Code not valid' } - } - - const { sub, ...data } = await plugin.config.generateTokenContents({ email }) - - const bearerToken = await signJwt({ - data, - expiresInSeconds: Auth.config.bearerTokenExpiryInSeconds, - sub, - }) - - const refreshToken = await generateRefreshToken({ sub }) - - if (Auth.config.cookies.isEnabled) { - setTokenCookies(honoContext, bearerToken, refreshToken) - } - - return { - __typename: 'LoginConfirmSuccessfulResponse', - bearerToken, - refreshToken, - } -} - -export default loginConfirm diff --git a/packages/auth-otp/graphql/Mutation/loginConfirm.test.ts b/packages/auth-otp/graphql/Mutation/loginConfirmWithEmail.test.ts similarity index 88% rename from packages/auth-otp/graphql/Mutation/loginConfirm.test.ts rename to packages/auth-otp/graphql/Mutation/loginConfirmWithEmail.test.ts index 4d992a30..697fc547 100644 --- a/packages/auth-otp/graphql/Mutation/loginConfirm.test.ts +++ b/packages/auth-otp/graphql/Mutation/loginConfirmWithEmail.test.ts @@ -3,14 +3,14 @@ import { describe, beforeEach, it, expect, } from 'bun:test' -import { LoginRequestMutation } from './loginRequest.test' +import { LoginRequestMutation } from './loginRequestWithEmail.test' import { loginRequestKeyValue } from '../../clients/loginRequestKeyValue' import plugin from '../../plugin' import { graphql } from '../client.generated' const LoginConfirmMutation = graphql(` mutation LoginConfirm($email: String!, $code: String!) { - loginConfirm(email: $email, code: $code) { + loginConfirmWithEmail(email: $email, code: $code) { __typename ... on LoginConfirmSuccessfulResponse { bearerToken @@ -36,7 +36,7 @@ describe('Mutation.loginConfirm', () => { const response = await app.gqlRequest(LoginConfirmMutation, { email, code: '000000' }) expect(response.data).toEqual({ - loginConfirm: { + loginConfirmWithEmail: { __typename: 'LoginConfirmSuccessfulResponse', bearerToken: expect.any(String), }, @@ -50,7 +50,7 @@ describe('Mutation.loginConfirm', () => { const response = await app.gqlRequest(LoginConfirmMutation, { email, code: '000000' }) expect(response.data).toEqual({ - loginConfirm: { + loginConfirmWithEmail: { __typename: 'CodeNotValidError', message: 'Must loginRequest code first, it might have expired', }, diff --git a/packages/auth-otp/graphql/Mutation/loginConfirmWithEmail.ts b/packages/auth-otp/graphql/Mutation/loginConfirmWithEmail.ts new file mode 100644 index 00000000..39e69b18 --- /dev/null +++ b/packages/auth-otp/graphql/Mutation/loginConfirmWithEmail.ts @@ -0,0 +1,19 @@ +import getTokens from '../../utils/getTokens' +import { isValidEmail } from '../../utils/isValidEmail' + +import type { + MutationResolvers, +} from '../schema.generated' + +export const loginConfirmWithEmail: NonNullable = async (_, { + email: emailIn, code, +}, { honoContext }) => { + const email = emailIn.toLowerCase().trim() + if (!emailIn || !isValidEmail(email)) { + return { __typename: 'EmailNotValidError', message: 'Email not valid' } + } + + return getTokens(code, emailIn, honoContext, 'email') +} + +export default loginConfirmWithEmail diff --git a/packages/auth-otp/graphql/Mutation/loginConfirmWithSms.ts b/packages/auth-otp/graphql/Mutation/loginConfirmWithSms.ts new file mode 100644 index 00000000..7c490a36 --- /dev/null +++ b/packages/auth-otp/graphql/Mutation/loginConfirmWithSms.ts @@ -0,0 +1,20 @@ +import getTokens from '../../utils/getTokens' +import { isValidE164Number } from '../../utils/isValidE164Number' + +import type { + MutationResolvers, +} from '../schema.generated' + +export const loginConfirmWithSms: NonNullable = async (_, { + phoneNumberWithCountryCode: phoneNumberIn, code, +}, { honoContext }) => { + const phoneNumberWithCountryCode = phoneNumberIn.trim() + + if (!isValidE164Number(phoneNumberWithCountryCode)) { + return { __typename: 'PhoneNumNotValidError', message: 'Phone number is not valid' } + } + + return getTokens(code, phoneNumberWithCountryCode, honoContext, 'sms') +} + +export default loginConfirmWithSms diff --git a/packages/auth-otp/graphql/Mutation/loginRequest.ts b/packages/auth-otp/graphql/Mutation/loginRequest.ts deleted file mode 100644 index 441d14f6..00000000 --- a/packages/auth-otp/graphql/Mutation/loginRequest.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { loginRequestKeyValue } from '../../clients/loginRequestKeyValue' -import plugin from '../../plugin' -import { isValidEmail } from '../../utils/isValidEmail' - -import type { MutationResolvers } from '../schema.generated' - -const getTwoFactorCode = () => { - if (process.env.NODE_ENV === 'test') { - return '000000' - } - const twoFactorCode = Math.floor(100000 + Math.random() * 900000).toString() - - return twoFactorCode -} - -export const loginRequest: NonNullable = async (_, { - email: emailInput, -}, context) => { - if (!isValidEmail(emailInput)) { - return { message: 'Not a valid email', __typename: 'EmailNotValidError' } - } - - const email = emailInput.toLowerCase().trim() - - const whitelistedEmailFromDomain = plugin.config.WHITELISTED_SIGNUP_EMAIL_DOMAINS?.includes(emailInput.split('@')[1]!) - - const validatedEmailFromWhitelist = plugin.config.WHITELISTED_SIGNUP_EMAILS?.includes(email) - - const hasWhitelist = plugin.config.WHITELISTED_SIGNUP_EMAILS || plugin.config.WHITELISTED_SIGNUP_EMAIL_DOMAINS - - if (hasWhitelist && !whitelistedEmailFromDomain && !validatedEmailFromWhitelist) { - return { - message: 'Email not whitelisted', - __typename: 'EmailNotValidError', - } - } - - const existing = await loginRequestKeyValue().get(email) - - if (existing?.loginRequestedAt) { - const { loginRequestedAt } = existing, - timeUntilAllowedToSendAnother = new Date(loginRequestedAt).valueOf() + (plugin.config.minTimeBetweenTwoFactorCodeRequestsInSeconds * 1000) - Date.now() - - if (timeUntilAllowedToSendAnother > 0) { - return { - success: false, - __typename: 'LoginRequestSuccessResponse', - } - } - } - - const twoFactorCode = getTwoFactorCode() - - await loginRequestKeyValue().set(email, { - loginRequestedAt: new Date().toISOString(), - twoFactorCode, - }, plugin.config.twoFactorCodeExpiryInSeconds) - - await plugin.config.handleAuthRequest({ email: emailInput }, twoFactorCode, context) - - return { - __typename: 'LoginRequestSuccessResponse', - success: true, - } -} - -export default loginRequest diff --git a/packages/auth-otp/graphql/Mutation/loginRequest.test.ts b/packages/auth-otp/graphql/Mutation/loginRequestWithEmail.test.ts similarity index 90% rename from packages/auth-otp/graphql/Mutation/loginRequest.test.ts rename to packages/auth-otp/graphql/Mutation/loginRequestWithEmail.test.ts index 8eaea809..a227dd43 100644 --- a/packages/auth-otp/graphql/Mutation/loginRequest.test.ts +++ b/packages/auth-otp/graphql/Mutation/loginRequestWithEmail.test.ts @@ -6,7 +6,7 @@ import { graphql } from '../client.generated' export const LoginRequestMutation = graphql(` mutation LoginRequest($email: String!) { - loginRequest(email: $email) { + loginRequestWithEmail(email: $email) { __typename ... on Error { message @@ -24,7 +24,7 @@ describe('Mutation.loginRequest', () => { const { data } = await app.gqlRequest(LoginRequestMutation, { email }) expect(data).toEqual({ - loginRequest: { + loginRequestWithEmail: { __typename: 'LoginRequestSuccessResponse', }, }) @@ -38,7 +38,7 @@ describe('Mutation.loginRequest', () => { const { data } = await app.gqlRequest(LoginRequestMutation, { email }) expect(data).toEqual({ - loginRequest: { + loginRequestWithEmail: { __typename: 'EmailNotValidError', message: 'Not a valid email', }, diff --git a/packages/auth-otp/graphql/Mutation/loginRequestWithEmail.ts b/packages/auth-otp/graphql/Mutation/loginRequestWithEmail.ts new file mode 100644 index 00000000..028d67f2 --- /dev/null +++ b/packages/auth-otp/graphql/Mutation/loginRequestWithEmail.ts @@ -0,0 +1,32 @@ +import plugin from '../../plugin' +import { isValidEmail } from '../../utils/isValidEmail' +import sendTwoFactorCode from '../../utils/sendTwoFactorCode' + +import type { MutationResolvers } from '../schema.generated' + +export const loginRequestWithEmail: NonNullable = async (_, { + email: emailInput, +}, context) => { + if (!isValidEmail(emailInput)) { + return { message: 'Not a valid email', __typename: 'EmailNotValidError' } + } + + const email = emailInput.toLowerCase().trim() + + const whitelistedEmailFromDomain = plugin.config.WHITELISTED_SIGNUP_EMAIL_DOMAINS?.includes(emailInput.split('@')[1]!) + + const validatedEmailFromWhitelist = plugin.config.WHITELISTED_SIGNUP_EMAILS?.includes(email) + + const hasWhitelist = plugin.config.WHITELISTED_SIGNUP_EMAILS || plugin.config.WHITELISTED_SIGNUP_EMAIL_DOMAINS + + if (hasWhitelist && !whitelistedEmailFromDomain && !validatedEmailFromWhitelist) { + return { + message: 'Email not whitelisted', + __typename: 'EmailNotValidError', + } + } + + return sendTwoFactorCode(emailInput, context, 'email') +} + +export default loginRequestWithEmail diff --git a/packages/auth-otp/graphql/Mutation/loginRequestWithSms.ts b/packages/auth-otp/graphql/Mutation/loginRequestWithSms.ts new file mode 100644 index 00000000..15e23400 --- /dev/null +++ b/packages/auth-otp/graphql/Mutation/loginRequestWithSms.ts @@ -0,0 +1,33 @@ +import { parsePhoneNumber } from 'libphonenumber-js' + +import plugin from '../../plugin' +import { isValidE164Number } from '../../utils/isValidE164Number' +import sendTwoFactorCode from '../../utils/sendTwoFactorCode' + +import type { MutationResolvers } from '../schema.generated' + +export const loginRequestWithSms: NonNullable = async (_, { + phoneNumberWithCountryCode: phoneNumIn, +}, context) => { + const phoneNumberWithCountryCode = phoneNumIn.trim() + + if (!isValidE164Number(phoneNumberWithCountryCode)) { + return { message: 'Not a valid phone number', __typename: 'PhoneNumNotValidError' } + } + + const { country } = parsePhoneNumber(phoneNumberWithCountryCode), + { WHITELISTED_COUNTRY_CODES } = plugin.config + + if (WHITELISTED_COUNTRY_CODES && WHITELISTED_COUNTRY_CODES.length) { + if (!country || !WHITELISTED_COUNTRY_CODES.includes(country)) { + return { + message: 'Country code not whitelisted', + __typename: 'PhoneNumNotValidError', + } + } + } + + return sendTwoFactorCode(phoneNumberWithCountryCode, context, 'sms') +} + +export default loginRequestWithSms diff --git a/packages/auth-otp/graphql/schema.generated.ts b/packages/auth-otp/graphql/schema.generated.ts index cbba6f00..1a39905c 100644 --- a/packages/auth-otp/graphql/schema.generated.ts +++ b/packages/auth-otp/graphql/schema.generated.ts @@ -40,32 +40,38 @@ export type Error = { readonly message: Scalars['String']['output']; }; -export type LoginConfirmResponse = CodeNotValidError | EmailNotValidError | LoginConfirmSuccessfulResponse | LoginFailedError; - export type LoginConfirmSuccessfulResponse = { readonly __typename?: 'LoginConfirmSuccessfulResponse'; readonly bearerToken: Scalars['String']['output']; readonly refreshToken: Scalars['String']['output']; }; +export type LoginConfirmWithEmailResponse = CodeNotValidError | EmailNotValidError | LoginConfirmSuccessfulResponse | LoginFailedError; + +export type LoginConfirmWithSmsResponse = CodeNotValidError | LoginConfirmSuccessfulResponse | LoginFailedError | PhoneNumNotValidError; + export type LoginFailedError = Error & { readonly __typename?: 'LoginFailedError'; readonly message: Scalars['String']['output']; }; -export type LoginRequestResponse = EmailNotValidError | LoginRequestSuccessResponse; - export type LoginRequestSuccessResponse = { readonly __typename?: 'LoginRequestSuccessResponse'; readonly success: Scalars['Boolean']['output']; }; +export type LoginRequestWithEmailResponse = EmailNotValidError | LoginRequestSuccessResponse; + +export type LoginRequestWithSmsResponse = LoginRequestSuccessResponse | PhoneNumNotValidError; + export type Mutation = { readonly __typename?: 'Mutation'; readonly clear: Scalars['Boolean']['output']; readonly delete: Scalars['Boolean']['output']; - readonly loginConfirm: LoginConfirmResponse; - readonly loginRequest: LoginRequestResponse; + readonly loginConfirmWithEmail: LoginConfirmWithEmailResponse; + readonly loginConfirmWithSms: LoginConfirmWithSmsResponse; + readonly loginRequestWithEmail: LoginRequestWithEmailResponse; + readonly loginRequestWithSms: LoginRequestWithSmsResponse; readonly logout: Scalars['DateTime']['output']; readonly logoutFromAllDevices: Scalars['DateTime']['output']; readonly refreshToken: NewTokenResponse; @@ -84,17 +90,28 @@ export type MutationDeleteArgs = { }; -export type MutationLoginConfirmArgs = { +export type MutationLoginConfirmWithEmailArgs = { code: Scalars['String']['input']; email: Scalars['String']['input']; }; -export type MutationLoginRequestArgs = { +export type MutationLoginConfirmWithSmsArgs = { + code: Scalars['String']['input']; + phoneNumberWithCountryCode: Scalars['String']['input']; +}; + + +export type MutationLoginRequestWithEmailArgs = { email: Scalars['String']['input']; }; +export type MutationLoginRequestWithSmsArgs = { + phoneNumberWithCountryCode: Scalars['String']['input']; +}; + + export type MutationRefreshTokenArgs = { bearerToken: Scalars['String']['input']; refreshToken: Scalars['String']['input']; @@ -116,6 +133,11 @@ export type NewTokenSuccessResponse = { readonly refreshToken: Scalars['String']['output']; }; +export type PhoneNumNotValidError = Error & { + readonly __typename?: 'PhoneNumNotValidError'; + readonly message: Scalars['String']['output']; +}; + export type Query = { readonly __typename?: 'Query'; readonly entries: ReadonlyArray; @@ -246,14 +268,16 @@ export type DirectiveResolverFn> = ResolversObject<{ - LoginConfirmResponse: ( CodeNotValidError ) | ( EmailNotValidError ) | ( LoginConfirmSuccessfulResponse ) | ( LoginFailedError ); - LoginRequestResponse: ( EmailNotValidError ) | ( LoginRequestSuccessResponse ); + LoginConfirmWithEmailResponse: ( CodeNotValidError ) | ( EmailNotValidError ) | ( LoginConfirmSuccessfulResponse ) | ( LoginFailedError ); + LoginConfirmWithSmsResponse: ( CodeNotValidError ) | ( LoginConfirmSuccessfulResponse ) | ( LoginFailedError ) | ( PhoneNumNotValidError ); + LoginRequestWithEmailResponse: ( EmailNotValidError ) | ( LoginRequestSuccessResponse ); + LoginRequestWithSmsResponse: ( LoginRequestSuccessResponse ) | ( PhoneNumNotValidError ); NewTokenResponse: ( NewTokenSuccessResponse ) | ( RefreshTokenInvalidError ); }>; /** Mapping of interface types */ export type ResolversInterfaceTypes> = ResolversObject<{ - Error: ( CodeNotValidError ) | ( EmailNotValidError ) | ( LoginFailedError ); + Error: ( CodeNotValidError ) | ( EmailNotValidError ) | ( LoginFailedError ) | ( PhoneNumNotValidError ); }>; /** Mapping between all available schema types and the resolvers types */ @@ -267,14 +291,17 @@ export type ResolversTypes = ResolversObject<{ Int: ResolverTypeWrapper; JSON: ResolverTypeWrapper; JSONObject: ResolverTypeWrapper; - LoginConfirmResponse: ResolverTypeWrapper['LoginConfirmResponse']>; LoginConfirmSuccessfulResponse: ResolverTypeWrapper; + LoginConfirmWithEmailResponse: ResolverTypeWrapper['LoginConfirmWithEmailResponse']>; + LoginConfirmWithSmsResponse: ResolverTypeWrapper['LoginConfirmWithSmsResponse']>; LoginFailedError: ResolverTypeWrapper; - LoginRequestResponse: ResolverTypeWrapper['LoginRequestResponse']>; LoginRequestSuccessResponse: ResolverTypeWrapper; + LoginRequestWithEmailResponse: ResolverTypeWrapper['LoginRequestWithEmailResponse']>; + LoginRequestWithSmsResponse: ResolverTypeWrapper['LoginRequestWithSmsResponse']>; Mutation: ResolverTypeWrapper<{}>; NewTokenResponse: ResolverTypeWrapper['NewTokenResponse']>; NewTokenSuccessResponse: ResolverTypeWrapper; + PhoneNumNotValidError: ResolverTypeWrapper; Query: ResolverTypeWrapper<{}>; RefreshTokenInvalidError: ResolverTypeWrapper; String: ResolverTypeWrapper; @@ -291,14 +318,17 @@ export type ResolversParentTypes = ResolversObject<{ Int: Scalars['Int']['output']; JSON: Scalars['JSON']['output']; JSONObject: Scalars['JSONObject']['output']; - LoginConfirmResponse: ResolversUnionTypes['LoginConfirmResponse']; LoginConfirmSuccessfulResponse: LoginConfirmSuccessfulResponse; + LoginConfirmWithEmailResponse: ResolversUnionTypes['LoginConfirmWithEmailResponse']; + LoginConfirmWithSmsResponse: ResolversUnionTypes['LoginConfirmWithSmsResponse']; LoginFailedError: LoginFailedError; - LoginRequestResponse: ResolversUnionTypes['LoginRequestResponse']; LoginRequestSuccessResponse: LoginRequestSuccessResponse; + LoginRequestWithEmailResponse: ResolversUnionTypes['LoginRequestWithEmailResponse']; + LoginRequestWithSmsResponse: ResolversUnionTypes['LoginRequestWithSmsResponse']; Mutation: {}; NewTokenResponse: ResolversUnionTypes['NewTokenResponse']; NewTokenSuccessResponse: NewTokenSuccessResponse; + PhoneNumNotValidError: PhoneNumNotValidError; Query: {}; RefreshTokenInvalidError: RefreshTokenInvalidError; String: Scalars['String']['output']; @@ -328,7 +358,7 @@ export type EmailNotValidErrorResolvers; export type ErrorResolvers = ResolversObject<{ - __resolveType: TypeResolveFn<'CodeNotValidError' | 'EmailNotValidError' | 'LoginFailedError', ParentType, ContextType>; + __resolveType: TypeResolveFn<'CodeNotValidError' | 'EmailNotValidError' | 'LoginFailedError' | 'PhoneNumNotValidError', ParentType, ContextType>; message?: Resolver>; }>; @@ -340,35 +370,45 @@ export interface JsonObjectScalarConfig extends GraphQLScalarTypeConfig = ResolversObject<{ - __resolveType: TypeResolveFn<'CodeNotValidError' | 'EmailNotValidError' | 'LoginConfirmSuccessfulResponse' | 'LoginFailedError', ParentType, ContextType>; -}>; - export type LoginConfirmSuccessfulResponseResolvers = ResolversObject<{ bearerToken?: Resolver>; refreshToken?: Resolver>; __isTypeOf?: IsTypeOfResolverFn; }>; +export type LoginConfirmWithEmailResponseResolvers = ResolversObject<{ + __resolveType: TypeResolveFn<'CodeNotValidError' | 'EmailNotValidError' | 'LoginConfirmSuccessfulResponse' | 'LoginFailedError', ParentType, ContextType>; +}>; + +export type LoginConfirmWithSmsResponseResolvers = ResolversObject<{ + __resolveType: TypeResolveFn<'CodeNotValidError' | 'LoginConfirmSuccessfulResponse' | 'LoginFailedError' | 'PhoneNumNotValidError', ParentType, ContextType>; +}>; + export type LoginFailedErrorResolvers = ResolversObject<{ message?: Resolver>; __isTypeOf?: IsTypeOfResolverFn; }>; -export type LoginRequestResponseResolvers = ResolversObject<{ - __resolveType: TypeResolveFn<'EmailNotValidError' | 'LoginRequestSuccessResponse', ParentType, ContextType>; -}>; - export type LoginRequestSuccessResponseResolvers = ResolversObject<{ success?: Resolver>; __isTypeOf?: IsTypeOfResolverFn; }>; +export type LoginRequestWithEmailResponseResolvers = ResolversObject<{ + __resolveType: TypeResolveFn<'EmailNotValidError' | 'LoginRequestSuccessResponse', ParentType, ContextType>; +}>; + +export type LoginRequestWithSmsResponseResolvers = ResolversObject<{ + __resolveType: TypeResolveFn<'LoginRequestSuccessResponse' | 'PhoneNumNotValidError', ParentType, ContextType>; +}>; + export type MutationResolvers = ResolversObject<{ clear?: Resolver>; delete?: Resolver>; - loginConfirm?: Resolver, RequireFields>; - loginRequest?: Resolver, RequireFields>; + loginConfirmWithEmail?: Resolver, RequireFields>; + loginConfirmWithSms?: Resolver, RequireFields>; + loginRequestWithEmail?: Resolver, RequireFields>; + loginRequestWithSms?: Resolver, RequireFields>; logout?: Resolver; logoutFromAllDevices?: Resolver; refreshToken?: Resolver, RequireFields>; @@ -385,6 +425,11 @@ export type NewTokenSuccessResponseResolvers; }>; +export type PhoneNumNotValidErrorResolvers = ResolversObject<{ + message?: Resolver>; + __isTypeOf?: IsTypeOfResolverFn; +}>; + export type QueryResolvers = ResolversObject<{ entries?: Resolver, ParentType, ContextType, RequireFields>; get?: Resolver, ParentType, ContextType, RequireFields>; @@ -409,14 +454,17 @@ export type Resolvers = ResolversObject<{ Error?: ErrorResolvers; JSON?: GraphQLScalarType; JSONObject?: GraphQLScalarType; - LoginConfirmResponse?: LoginConfirmResponseResolvers; LoginConfirmSuccessfulResponse?: LoginConfirmSuccessfulResponseResolvers; + LoginConfirmWithEmailResponse?: LoginConfirmWithEmailResponseResolvers; + LoginConfirmWithSmsResponse?: LoginConfirmWithSmsResponseResolvers; LoginFailedError?: LoginFailedErrorResolvers; - LoginRequestResponse?: LoginRequestResponseResolvers; LoginRequestSuccessResponse?: LoginRequestSuccessResponseResolvers; + LoginRequestWithEmailResponse?: LoginRequestWithEmailResponseResolvers; + LoginRequestWithSmsResponse?: LoginRequestWithSmsResponseResolvers; Mutation?: MutationResolvers; NewTokenResponse?: NewTokenResponseResolvers; NewTokenSuccessResponse?: NewTokenSuccessResponseResolvers; + PhoneNumNotValidError?: PhoneNumNotValidErrorResolvers; Query?: QueryResolvers; RefreshTokenInvalidError?: RefreshTokenInvalidErrorResolvers; }>; diff --git a/packages/auth-otp/graphql/schema.graphql b/packages/auth-otp/graphql/schema.graphql index b497c481..eaf9bdfe 100644 --- a/packages/auth-otp/graphql/schema.graphql +++ b/packages/auth-otp/graphql/schema.graphql @@ -13,8 +13,10 @@ directive @auth( ) on FIELD_DEFINITION type Mutation { - loginRequest(email: String!): LoginRequestResponse! @auth(skip: true) - loginConfirm(email: String!, code: String!): LoginConfirmResponse! @auth(skip: true) + loginRequestWithEmail(email: String!): LoginRequestWithEmailResponse! @auth(skip: true) + loginConfirmWithEmail(email: String!, code: String!): LoginConfirmWithEmailResponse! @auth(skip: true) + loginRequestWithSms(phoneNumberWithCountryCode: String!): LoginRequestWithSmsResponse! @auth(skip: true) + loginConfirmWithSms(phoneNumberWithCountryCode: String!, code: String!): LoginConfirmWithSmsResponse! @auth(skip: true) } type LoginRequestSuccessResponse { @@ -25,6 +27,10 @@ type EmailNotValidError implements Error { message: String! @auth(skip: true) } +type PhoneNumNotValidError implements Error { + message: String! @auth(skip: true) +} + type CodeNotValidError implements Error { message: String! @auth(skip: true) } @@ -33,18 +39,24 @@ type LoginFailedError implements Error { message: String! @auth(skip: true) } -union LoginRequestResponse = LoginRequestSuccessResponse | EmailNotValidError +union LoginRequestWithEmailResponse = LoginRequestSuccessResponse | EmailNotValidError +union LoginRequestWithSmsResponse = LoginRequestSuccessResponse | PhoneNumNotValidError type LoginConfirmSuccessfulResponse { bearerToken: String! @auth(skip: true) refreshToken: String! @auth(skip: true) } -union LoginConfirmResponse = LoginConfirmSuccessfulResponse +union LoginConfirmWithEmailResponse = LoginConfirmSuccessfulResponse | EmailNotValidError | CodeNotValidError | LoginFailedError +union LoginConfirmWithSmsResponse = LoginConfirmSuccessfulResponse + | PhoneNumNotValidError + | CodeNotValidError + | LoginFailedError + interface Error { message: String! @auth(skip: true) } diff --git a/packages/auth-otp/package.json b/packages/auth-otp/package.json index 4a974926..15ee5e59 100644 --- a/packages/auth-otp/package.json +++ b/packages/auth-otp/package.json @@ -36,12 +36,13 @@ "dependencies": { "@envelop/core": "^5.0.0", "@envelop/generic-auth": "^7.0.0", - "@zemble/core": "workspace:*", - "@zemble/graphql": "workspace:*", "@whatwg-node/server-plugin-cookies": "^1.0.2", - "graphql": "^16.8.1", "@zemble/auth": "workspace:*", + "@zemble/core": "workspace:*", + "@zemble/graphql": "workspace:*", + "@zemble/kv": "workspace:*", "@zemble/utils": "workspace:*", - "@zemble/kv": "workspace:*" + "graphql": "^16.8.1", + "libphonenumber-js": "^1.11.2" } } diff --git a/packages/auth-otp/plugin.ts b/packages/auth-otp/plugin.ts index ead151b1..f19ee057 100644 --- a/packages/auth-otp/plugin.ts +++ b/packages/auth-otp/plugin.ts @@ -5,17 +5,22 @@ import { Plugin } from '@zemble/core' import GraphQL from '@zemble/graphql' import kv from '@zemble/kv' import { parseEnvJSON } from '@zemble/utils/node/parseEnv' +import { type CountryCode } from 'libphonenumber-js' import { simpleTemplating } from './utils/simpleTemplating' +import type { E164PhoneNumber } from './utils/types' import type { IEmail } from '@zemble/core' interface OtpAuthConfig extends Zemble.GlobalConfig { readonly twoFactorCodeExpiryInSeconds?: number readonly minTimeBetweenTwoFactorCodeRequestsInSeconds?: number + + // EMAIL readonly WHITELISTED_SIGNUP_EMAILS?: readonly string[] readonly WHITELISTED_SIGNUP_EMAIL_DOMAINS?: readonly string[] - readonly from: IEmail + readonly fromEmail?: IEmail + /* * Write {{email}}, {{name}} and {{twoFactorCode}} to have them replaced with the * email, name and two factor code. @@ -31,13 +36,34 @@ interface OtpAuthConfig extends Zemble.GlobalConfig { * email, name and two factor code. */ readonly emailHtml?: string - readonly handleAuthRequest?: (email: IEmail, twoFactorCode: string, context: Zemble.GlobalContext) => Promise | void - readonly generateTokenContents: ({ email }: {readonly email: string}) => Promise> | Omit + + // SMS + + /* + * The name that the SMS will appear to be sent from. + * Should be a E.164 formatted phone number (e.g. +14155552671) or a string (company name). + */ + readonly fromSms?: string + /* + * Write {{twoFactorCode}} to have them replaced with the + * email, name and two factor code. + */ + readonly smsMessage?: string + /* + * A list of two-letter country codes in ISO alpha-2 format that are allowed to receive SMS messages. + * If this is not set, all country codes are allowed. + */ + readonly WHITELISTED_COUNTRY_CODES?: readonly CountryCode[] + + readonly handleEmailAuthRequest?: (email: IEmail, twoFactorCode: string, context: Zemble.GlobalContext) => Promise | void + readonly handleSmsAuthRequest?: (phoneNum: E164PhoneNumber, twoFactorCode: string, context: Zemble.GlobalContext) => Promise | void + readonly generateTokenContents: ({ email, phoneNumber }: GenerateTokenContentArgs) => Promise> | Omit } export interface DefaultOtpToken { // readonly type: 'AuthOtp', - readonly email: string, + readonly email?: string, + readonly phoneNumber?: string, readonly sub: string } @@ -53,10 +79,12 @@ declare global { } } -function generateTokenContents({ email }: {readonly email: string}): Zemble.OtpToken { +type GenerateTokenContentArgs = {readonly email?: string, readonly phoneNumber?: string} + +function generateTokenContents({ email, phoneNumber }: GenerateTokenContentArgs): Zemble.OtpToken { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - this is a default implementation - return { email, type: 'AuthOtp' as const } + return { email, phoneNumber, type: 'AuthOtp' as const } } const defaultConfig = { @@ -65,11 +93,16 @@ const defaultConfig = { generateTokenContents, WHITELISTED_SIGNUP_EMAIL_DOMAINS: parseEnvJSON('WHITELISTED_SIGNUP_EMAIL_DOMAINS', undefined), WHITELISTED_SIGNUP_EMAILS: parseEnvJSON('WHITELISTED_SIGNUP_EMAILS', undefined), - handleAuthRequest: async (to, twoFactorCode) => { + handleEmailAuthRequest: async (to, twoFactorCode) => { const { sendEmail } = plugin.providers + + if (!plugin.config.fromEmail) { + throw new Error('fromEmail must be set') + } + if (sendEmail && !['test', 'development'].includes(process.env.NODE_ENV ?? '')) { void sendEmail({ - from: plugin.config.from, + from: plugin.config.fromEmail, subject: plugin.config.emailSubject ? simpleTemplating(plugin.config.emailSubject, { email: to.email, name: to.name ?? to.email, twoFactorCode }) : 'Login', text: plugin.config.emailText ? simpleTemplating(plugin.config.emailText, { email: to.email, name: to.name ?? to.email, twoFactorCode }) : `Your two factor code is ${twoFactorCode}`, html: plugin.config.emailHtml ? simpleTemplating(plugin.config.emailHtml, { email: to.email, name: to.name ?? to.email, twoFactorCode }) : `Your two factor code is ${twoFactorCode}`, @@ -80,6 +113,28 @@ const defaultConfig = { plugin.providers.logger.info(`Generated code for ${to.email}: ${twoFactorCode}`) } }, + + handleSmsAuthRequest: async (to, twoFactorCode) => { + const { sendSms } = plugin.providers, + { fromSms: from, smsMessage } = plugin.config + + if (!from) { + throw new Error('fromSms must be set') + } + + const message = smsMessage ? simpleTemplating(smsMessage, { twoFactorCode }) : `Your code is ${twoFactorCode}` + + if (sendSms && !['test', 'development'].includes(process.env.NODE_ENV ?? '')) { + void sendSms({ + from, + message, + to, + }) + } + if (process.env.NODE_ENV === 'development') { + plugin.providers.logger.info(`Generated code for ${to}: ${twoFactorCode}`) + } + }, } satisfies Partial /** @@ -102,9 +157,11 @@ const plugin = new Plugin(import.meta.dir, ], defaultConfig, additionalConfigWhenRunningLocally: { - handleAuthRequest: ({ email }, code, { logger }) => { logger.info(`handleAuthRequest for ${email}`, code) }, + handleEmailAuthRequest: ({ email }, code, { logger }) => { logger.info(`handleAuthRequest for ${email}`, code) }, + handleSmsAuthRequest: (to, code, { logger }) => { logger.info(`handleAuthRequest for ${to}`, code) }, generateTokenContents, - from: { email: 'noreply@zemble.com' }, + fromEmail: { email: 'noreply@zemble.com' }, + fromSms: 'Zemble', }, }) diff --git a/packages/auth-otp/utils/getTokens.ts b/packages/auth-otp/utils/getTokens.ts new file mode 100644 index 00000000..a414abbd --- /dev/null +++ b/packages/auth-otp/utils/getTokens.ts @@ -0,0 +1,47 @@ +import Auth from '@zemble/auth' +import { generateRefreshToken } from '@zemble/auth/utils/generateRefreshToken' +import { setTokenCookies } from '@zemble/auth/utils/setBearerTokenCookie' +import { signJwt } from '@zemble/auth/utils/signJwt' + +import { loginRequestKeyValue } from '../clients/loginRequestKeyValue' +import plugin from '../plugin' + +const getTokens = async (code: string, emailOrPhoneNumber: string, honoContext: Zemble.RouteContext, signInMethod: 'email' | 'sms') => { + if (code.length !== 6) { + return { __typename: 'CodeNotValidError' as const, message: 'Code should be 6 characters' } + } + + const entry = await loginRequestKeyValue().get(emailOrPhoneNumber.toLowerCase()) + + if (!entry) { + return { __typename: 'CodeNotValidError' as const, message: 'Must loginRequest code first, it might have expired' } + } + + if (entry?.twoFactorCode !== code) { + return { __typename: 'CodeNotValidError' as const, message: 'Code not valid' } + } + + const generateTokenContentsArgs = signInMethod === 'email' ? { email: emailOrPhoneNumber } : { phoneNumber: emailOrPhoneNumber } + + const { sub, ...data } = await plugin.config.generateTokenContents(generateTokenContentsArgs) + + const bearerToken = await signJwt({ + data, + expiresInSeconds: Auth.config.bearerTokenExpiryInSeconds, + sub, + }) + + const refreshToken = await generateRefreshToken({ sub }) + + if (Auth.config.cookies.isEnabled) { + setTokenCookies(honoContext, bearerToken, refreshToken) + } + + return { + __typename: 'LoginConfirmSuccessfulResponse' as const, + bearerToken, + refreshToken, + } +} + +export default getTokens diff --git a/packages/auth-otp/utils/getTwoFactorCode.ts b/packages/auth-otp/utils/getTwoFactorCode.ts new file mode 100644 index 00000000..6adc78ce --- /dev/null +++ b/packages/auth-otp/utils/getTwoFactorCode.ts @@ -0,0 +1,10 @@ +const getTwoFactorCode = () => { + if (process.env.NODE_ENV === 'test') { + return '000000' + } + const twoFactorCode = Math.floor(100000 + Math.random() * 900000).toString() + + return twoFactorCode +} + +export default getTwoFactorCode diff --git a/packages/auth-otp/utils/isValidE164Number.ts b/packages/auth-otp/utils/isValidE164Number.ts new file mode 100644 index 00000000..37e2f736 --- /dev/null +++ b/packages/auth-otp/utils/isValidE164Number.ts @@ -0,0 +1,3 @@ +const e164Regex = /^\+?[1-9]\d{1,14}$/ + +export const isValidE164Number = (input: string) => e164Regex.test(input) diff --git a/packages/auth-otp/utils/sendTwoFactorCode.ts b/packages/auth-otp/utils/sendTwoFactorCode.ts new file mode 100644 index 00000000..9b71c5a5 --- /dev/null +++ b/packages/auth-otp/utils/sendTwoFactorCode.ts @@ -0,0 +1,47 @@ +import getTwoFactorCode from './getTwoFactorCode' +import { loginRequestKeyValue } from '../clients/loginRequestKeyValue' +import plugin from '../plugin' + +import type { E164PhoneNumber } from './types' + +type Context = Omit & { + readonly decodedToken: never; +} + +const sendTwoFactorCode = async (emailOrPhoneNumber: string, context: Context, signInMethod: 'email' | 'sms') => { + const existing = await loginRequestKeyValue().get(emailOrPhoneNumber) + + if (existing?.loginRequestedAt) { + const { loginRequestedAt } = existing, + timeUntilAllowedToSendAnother = new Date(loginRequestedAt).valueOf() + (plugin.config.minTimeBetweenTwoFactorCodeRequestsInSeconds * 1000) - Date.now() + + if (timeUntilAllowedToSendAnother > 0) { + return { + success: false, + __typename: 'LoginRequestSuccessResponse' as const, + } + } + } + + const twoFactorCode = getTwoFactorCode() + + await loginRequestKeyValue().set(emailOrPhoneNumber, { + loginRequestedAt: new Date().toISOString(), + twoFactorCode, + }, plugin.config.twoFactorCodeExpiryInSeconds) + + if (signInMethod === 'email') { + await plugin.config.handleEmailAuthRequest({ email: emailOrPhoneNumber }, twoFactorCode, context) + } + + if (signInMethod === 'sms') { + await plugin.config.handleSmsAuthRequest(emailOrPhoneNumber as E164PhoneNumber, twoFactorCode, context) + } + + return { + __typename: 'LoginRequestSuccessResponse' as const, + success: true, + } +} + +export default sendTwoFactorCode diff --git a/packages/auth-otp/utils/types.ts b/packages/auth-otp/utils/types.ts new file mode 100644 index 00000000..7e48237c --- /dev/null +++ b/packages/auth-otp/utils/types.ts @@ -0,0 +1,2 @@ +type OneToNine = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' +export type E164PhoneNumber = `+${OneToNine}${number}` diff --git a/packages/auth/.env.dev b/packages/auth/.env.dev index 30a5979d..134b71b3 100644 --- a/packages/auth/.env.dev +++ b/packages/auth/.env.dev @@ -1,67 +1,67 @@ PUBLIC_KEY='-----BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx38wcHxQwf01VagzmEtL -dSAcRR8jgVOHGSM15lUqwefH5T0zx+Z70WojleksUvbmcMGa0DklnBeIslJLDt2O -RtnMa1e2E8nlhFLScoxBjVWa8eae00u+o46y3+donnLiGhCRhEdx1Px/x0fVIaNo -DMlDM6CSFUCAq30CdG1eLLaU6FwdKJHybS2bOvTRXtLgH89eJkjsF2Rhzx6rxpm6 -Yp58uPkbV1454BBQUQxqBp4LwY8ra0sQeYxbde1XJAo2IGsaoICXh9kX6GIOXQT+ -XNR7wV7ifUaLpIT/XT60g9MCyRG/ZyCsEPI/qhMjd3jZqj0Iwmbr0Ijbkw7HRIud -5OWoGYmnMIF8bxukig+k2GDbwqfAw8LaDZ6CW0oOYpJHCC3y+SzRL5WEy9s2oAuu -dtpXrl5OFe2swIb1pacjhOqF5QTIz8Lcyq5bZtuUmN/aZtckctXqWfmoD0/JW+13 -uNbj2SF1XiAsTztDr6V/MrrBbCqFdwJUMfgJUiFx1yfxmAAuSq4oSEXM4TpTjeYd -5a2uYe0WS+5ePUlNQIcSCu42j5y4bAHaRSEvEnJwurcPBzJ26CSDaBmufLaSbgPF -QaXBaDJ5G2ZE/dGOCGNxrZCs4JOdC2VajiIGgMEAl8KwaXetwElufWLGm2Cu1DKO -fZjsKG+2Ul1ISOu9FQhZRZsCAwEAAQ== +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArRjgBxq2WSsxHvXeM0Gu +fa1s1Ovoo3UQfd239dO+vxc8H4FbO60KYkVwSPwTfqOvre5pp5XkyFFhHonNNrQa +qAzPCihYhIwvL0H7CvOaQqDCDGt5+UZnQ1gHxng9zTDOP3NKtmZ6d9VDHydK+5tW +3sObf2aLXA19W1r4s++SzWtr1zkHs+NJ2L8m0Jm/mDu45n1pz4eKZK43iTwUcpRz +SPfreK8lzKulhP+Ehr+1o6Px3y5C4uHdl0FN6B6bdbR/OfQuCDQ+KBIeW13epEep +0W675WIUF7c7lCyzstskRPkB9SITYTjvzEesWarQo9C9vr22AGCF70JJbuGoDAk2 +TpPEA1S4bZWZxwdTTV6Ys9iQd4dSWh39G+rnt7I2QMJhHmGnYA/PSDGxyDd9y5ub +prKGX5UERdGAPEZxgJjCRFhOZgCsYXMewg246oUpMe8Efvi67DqZqPHJmJK2++Cz +nEhrZXqEvSSpVvnP89RXutLCvOdFeWxx4NlPpjwI0Whs+JPrpFuqXKhRX7cbk33X +zD79jNdVXZEWDCwOkENeDqfPM5KiQD3y6geDAFghBge9Xwb9FMPCJ9GV2Zt2cbFL +PmS5NlwkjP5O9jHswPFZdsPT5oud/zrNB5OYf2J1qyUl0y6tu7ryH697p9PJECXP +pduX76EzFL7Qe9r9jlUNCm8CAwEAAQ== -----END PUBLIC KEY-----' PRIVATE_KEY='-----BEGIN PRIVATE KEY----- -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 -anuHtwf+9x+m+BAcpUIBMWBkO22afONp +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 +DmxKOUbG7CHn8nB21sC7cvKKEIxoCg== -----END PRIVATE KEY-----' \ No newline at end of file diff --git a/packages/auth/.env.test b/packages/auth/.env.test index 30a5979d..134b71b3 100644 --- a/packages/auth/.env.test +++ b/packages/auth/.env.test @@ -1,67 +1,67 @@ PUBLIC_KEY='-----BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx38wcHxQwf01VagzmEtL -dSAcRR8jgVOHGSM15lUqwefH5T0zx+Z70WojleksUvbmcMGa0DklnBeIslJLDt2O -RtnMa1e2E8nlhFLScoxBjVWa8eae00u+o46y3+donnLiGhCRhEdx1Px/x0fVIaNo -DMlDM6CSFUCAq30CdG1eLLaU6FwdKJHybS2bOvTRXtLgH89eJkjsF2Rhzx6rxpm6 -Yp58uPkbV1454BBQUQxqBp4LwY8ra0sQeYxbde1XJAo2IGsaoICXh9kX6GIOXQT+ -XNR7wV7ifUaLpIT/XT60g9MCyRG/ZyCsEPI/qhMjd3jZqj0Iwmbr0Ijbkw7HRIud -5OWoGYmnMIF8bxukig+k2GDbwqfAw8LaDZ6CW0oOYpJHCC3y+SzRL5WEy9s2oAuu -dtpXrl5OFe2swIb1pacjhOqF5QTIz8Lcyq5bZtuUmN/aZtckctXqWfmoD0/JW+13 -uNbj2SF1XiAsTztDr6V/MrrBbCqFdwJUMfgJUiFx1yfxmAAuSq4oSEXM4TpTjeYd -5a2uYe0WS+5ePUlNQIcSCu42j5y4bAHaRSEvEnJwurcPBzJ26CSDaBmufLaSbgPF -QaXBaDJ5G2ZE/dGOCGNxrZCs4JOdC2VajiIGgMEAl8KwaXetwElufWLGm2Cu1DKO -fZjsKG+2Ul1ISOu9FQhZRZsCAwEAAQ== +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArRjgBxq2WSsxHvXeM0Gu +fa1s1Ovoo3UQfd239dO+vxc8H4FbO60KYkVwSPwTfqOvre5pp5XkyFFhHonNNrQa +qAzPCihYhIwvL0H7CvOaQqDCDGt5+UZnQ1gHxng9zTDOP3NKtmZ6d9VDHydK+5tW +3sObf2aLXA19W1r4s++SzWtr1zkHs+NJ2L8m0Jm/mDu45n1pz4eKZK43iTwUcpRz +SPfreK8lzKulhP+Ehr+1o6Px3y5C4uHdl0FN6B6bdbR/OfQuCDQ+KBIeW13epEep +0W675WIUF7c7lCyzstskRPkB9SITYTjvzEesWarQo9C9vr22AGCF70JJbuGoDAk2 +TpPEA1S4bZWZxwdTTV6Ys9iQd4dSWh39G+rnt7I2QMJhHmGnYA/PSDGxyDd9y5ub +prKGX5UERdGAPEZxgJjCRFhOZgCsYXMewg246oUpMe8Efvi67DqZqPHJmJK2++Cz +nEhrZXqEvSSpVvnP89RXutLCvOdFeWxx4NlPpjwI0Whs+JPrpFuqXKhRX7cbk33X +zD79jNdVXZEWDCwOkENeDqfPM5KiQD3y6geDAFghBge9Xwb9FMPCJ9GV2Zt2cbFL +PmS5NlwkjP5O9jHswPFZdsPT5oud/zrNB5OYf2J1qyUl0y6tu7ryH697p9PJECXP +pduX76EzFL7Qe9r9jlUNCm8CAwEAAQ== -----END PUBLIC KEY-----' PRIVATE_KEY='-----BEGIN PRIVATE KEY----- -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 -anuHtwf+9x+m+BAcpUIBMWBkO22afONp +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 +DmxKOUbG7CHn8nB21sC7cvKKEIxoCg== -----END PRIVATE KEY-----' \ No newline at end of file diff --git a/packages/cms-users/plugin.ts b/packages/cms-users/plugin.ts index 890b6899..3d5a088f 100644 --- a/packages/cms-users/plugin.ts +++ b/packages/cms-users/plugin.ts @@ -61,7 +61,7 @@ const plugin = new Plugin(import.meta.dir, }, { plugin: authOtp.configure({ - from: { + fromEmail: { email: 'noreply@cmsexample.com', }, generateTokenContents: async ({ email }) => { diff --git a/packages/core/types.ts b/packages/core/types.ts index 0d2de34a..1543d4d3 100644 --- a/packages/core/types.ts +++ b/packages/core/types.ts @@ -28,6 +28,14 @@ export type SendEmailParams = { export type IStandardSendEmailService = (options: SendEmailParams) => Promise +export type SendSmsParams = { + readonly to: string, + readonly from: string, + readonly message: string, +} + +export type IStandardSendSmsService = (options: SendSmsParams) => Promise + export abstract class IStandardKeyValueService { abstract set(key: string, value: T, expireAfterSeconds?: number): Promise | void @@ -108,6 +116,8 @@ declare global { // eslint-disable-next-line functional/prefer-readonly-type sendEmail?: IStandardSendEmailService // eslint-disable-next-line functional/prefer-readonly-type + sendSms?: IStandardSendSmsService + // eslint-disable-next-line functional/prefer-readonly-type kv: (prefix: K) => IStandardKeyValueService // eslint-disable-next-line functional/prefer-readonly-type diff --git a/packages/push-expo/graphql/schema.generated.ts b/packages/push-expo/graphql/schema.generated.ts index 2435089b..fc038dad 100644 --- a/packages/push-expo/graphql/schema.generated.ts +++ b/packages/push-expo/graphql/schema.generated.ts @@ -20,7 +20,6 @@ export type Scalars = { export type Mutation = { readonly __typename?: 'Mutation'; - readonly randomNumber: Scalars['Int']['output']; readonly registerExpoPushToken: Scalars['Boolean']['output']; readonly sendPushNotification: Scalars['Boolean']['output']; }; @@ -44,23 +43,6 @@ export enum Platform { Web = 'web' } -export type Query = { - readonly __typename?: 'Query'; - readonly hello: Scalars['String']['output']; -}; - -export type Subscription = { - readonly __typename?: 'Subscription'; - readonly countdown: Scalars['Int']['output']; - readonly randomNumber: Scalars['Int']['output']; - readonly tick: Scalars['Float']['output']; -}; - - -export type SubscriptionCountdownArgs = { - from: Scalars['Int']['input']; -}; - export type WithIndex = TObject & Record; export type ResolversObject = WithIndex; @@ -134,45 +116,24 @@ export type DirectiveResolverFn; - Float: ResolverTypeWrapper; - Int: ResolverTypeWrapper; Mutation: ResolverTypeWrapper<{}>; Platform: Platform; - Query: ResolverTypeWrapper<{}>; String: ResolverTypeWrapper; - Subscription: ResolverTypeWrapper<{}>; }>; /** Mapping between all available schema types and the resolvers parents */ export type ResolversParentTypes = ResolversObject<{ Boolean: Scalars['Boolean']['output']; - Float: Scalars['Float']['output']; - Int: Scalars['Int']['output']; Mutation: {}; - Query: {}; String: Scalars['String']['output']; - Subscription: {}; }>; export type MutationResolvers = ResolversObject<{ - randomNumber?: Resolver; registerExpoPushToken?: Resolver>; sendPushNotification?: Resolver>; }>; -export type QueryResolvers = ResolversObject<{ - hello?: Resolver; -}>; - -export type SubscriptionResolvers = ResolversObject<{ - countdown?: SubscriptionResolver>; - randomNumber?: SubscriptionResolver; - tick?: SubscriptionResolver; -}>; - export type Resolvers = ResolversObject<{ Mutation?: MutationResolvers; - Query?: QueryResolvers; - Subscription?: SubscriptionResolvers; }>; diff --git a/packages/sms-46elks/.env b/packages/sms-46elks/.env new file mode 100644 index 00000000..92c85fc3 --- /dev/null +++ b/packages/sms-46elks/.env @@ -0,0 +1,3 @@ + +ELKS_USERNAME=u7c8ce96784a6310f788498d749bb9368 +ELKS_PASSWORD=5259CE2557B0631914FA64F4741F7B8F \ No newline at end of file diff --git a/packages/sms-46elks/.env.dev b/packages/sms-46elks/.env.dev new file mode 100644 index 00000000..134b71b3 --- /dev/null +++ b/packages/sms-46elks/.env.dev @@ -0,0 +1,67 @@ + +PUBLIC_KEY='-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArRjgBxq2WSsxHvXeM0Gu +fa1s1Ovoo3UQfd239dO+vxc8H4FbO60KYkVwSPwTfqOvre5pp5XkyFFhHonNNrQa +qAzPCihYhIwvL0H7CvOaQqDCDGt5+UZnQ1gHxng9zTDOP3NKtmZ6d9VDHydK+5tW +3sObf2aLXA19W1r4s++SzWtr1zkHs+NJ2L8m0Jm/mDu45n1pz4eKZK43iTwUcpRz +SPfreK8lzKulhP+Ehr+1o6Px3y5C4uHdl0FN6B6bdbR/OfQuCDQ+KBIeW13epEep +0W675WIUF7c7lCyzstskRPkB9SITYTjvzEesWarQo9C9vr22AGCF70JJbuGoDAk2 +TpPEA1S4bZWZxwdTTV6Ys9iQd4dSWh39G+rnt7I2QMJhHmGnYA/PSDGxyDd9y5ub +prKGX5UERdGAPEZxgJjCRFhOZgCsYXMewg246oUpMe8Efvi67DqZqPHJmJK2++Cz +nEhrZXqEvSSpVvnP89RXutLCvOdFeWxx4NlPpjwI0Whs+JPrpFuqXKhRX7cbk33X +zD79jNdVXZEWDCwOkENeDqfPM5KiQD3y6geDAFghBge9Xwb9FMPCJ9GV2Zt2cbFL +PmS5NlwkjP5O9jHswPFZdsPT5oud/zrNB5OYf2J1qyUl0y6tu7ryH697p9PJECXP +pduX76EzFL7Qe9r9jlUNCm8CAwEAAQ== +-----END PUBLIC KEY-----' +PRIVATE_KEY='-----BEGIN PRIVATE KEY----- +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 +DmxKOUbG7CHn8nB21sC7cvKKEIxoCg== +-----END PRIVATE KEY-----' \ No newline at end of file diff --git a/packages/sms-46elks/.env.test b/packages/sms-46elks/.env.test new file mode 100644 index 00000000..134b71b3 --- /dev/null +++ b/packages/sms-46elks/.env.test @@ -0,0 +1,67 @@ + +PUBLIC_KEY='-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArRjgBxq2WSsxHvXeM0Gu +fa1s1Ovoo3UQfd239dO+vxc8H4FbO60KYkVwSPwTfqOvre5pp5XkyFFhHonNNrQa +qAzPCihYhIwvL0H7CvOaQqDCDGt5+UZnQ1gHxng9zTDOP3NKtmZ6d9VDHydK+5tW +3sObf2aLXA19W1r4s++SzWtr1zkHs+NJ2L8m0Jm/mDu45n1pz4eKZK43iTwUcpRz +SPfreK8lzKulhP+Ehr+1o6Px3y5C4uHdl0FN6B6bdbR/OfQuCDQ+KBIeW13epEep +0W675WIUF7c7lCyzstskRPkB9SITYTjvzEesWarQo9C9vr22AGCF70JJbuGoDAk2 +TpPEA1S4bZWZxwdTTV6Ys9iQd4dSWh39G+rnt7I2QMJhHmGnYA/PSDGxyDd9y5ub +prKGX5UERdGAPEZxgJjCRFhOZgCsYXMewg246oUpMe8Efvi67DqZqPHJmJK2++Cz +nEhrZXqEvSSpVvnP89RXutLCvOdFeWxx4NlPpjwI0Whs+JPrpFuqXKhRX7cbk33X +zD79jNdVXZEWDCwOkENeDqfPM5KiQD3y6geDAFghBge9Xwb9FMPCJ9GV2Zt2cbFL +PmS5NlwkjP5O9jHswPFZdsPT5oud/zrNB5OYf2J1qyUl0y6tu7ryH697p9PJECXP +pduX76EzFL7Qe9r9jlUNCm8CAwEAAQ== +-----END PUBLIC KEY-----' +PRIVATE_KEY='-----BEGIN PRIVATE KEY----- +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 +DmxKOUbG7CHn8nB21sC7cvKKEIxoCg== +-----END PRIVATE KEY-----' \ No newline at end of file diff --git a/packages/sms-46elks/.eslintrc b/packages/sms-46elks/.eslintrc new file mode 100644 index 00000000..5547e0ef --- /dev/null +++ b/packages/sms-46elks/.eslintrc @@ -0,0 +1,5 @@ +{ + "extends": [ + "../../.eslintrc" + ] +} diff --git a/packages/sms-46elks/.gitignore b/packages/sms-46elks/.gitignore new file mode 100644 index 00000000..e6905a23 --- /dev/null +++ b/packages/sms-46elks/.gitignore @@ -0,0 +1 @@ +.env* \ No newline at end of file diff --git a/packages/sms-46elks/README.md b/packages/sms-46elks/README.md new file mode 100644 index 00000000..7fc63c4b --- /dev/null +++ b/packages/sms-46elks/README.md @@ -0,0 +1,7 @@ +# @zemble/sms-46elks + +This package is a @zemble plugin to easily send SMS messages using the 46elks API. + +Read more about [46Elks](https://46elks.com/) + +Read more about [@zemble](https://github.com/kingstinct/zemble) \ No newline at end of file diff --git a/packages/sms-46elks/codegen.ts b/packages/sms-46elks/codegen.ts new file mode 100644 index 00000000..2153aff7 --- /dev/null +++ b/packages/sms-46elks/codegen.ts @@ -0,0 +1,7 @@ +import defaultConfig from '@zemble/graphql/codegen' + +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = defaultConfig + +export default config diff --git a/packages/sms-46elks/graphql/Mutation/sendSms.ts b/packages/sms-46elks/graphql/Mutation/sendSms.ts new file mode 100644 index 00000000..fee3e3b9 --- /dev/null +++ b/packages/sms-46elks/graphql/Mutation/sendSms.ts @@ -0,0 +1,7 @@ +import plugin from '../../plugin' + +import type { MutationResolvers } from '../schema.generated' + +export const sendSms: NonNullable = async (_, data) => plugin.providers.sendSms(data) + +export default sendSms diff --git a/packages/sms-46elks/graphql/schema.generated.ts b/packages/sms-46elks/graphql/schema.generated.ts new file mode 100644 index 00000000..c44cc402 --- /dev/null +++ b/packages/sms-46elks/graphql/schema.generated.ts @@ -0,0 +1,124 @@ +// @ts-nocheck +import '@zemble/core' +import type { GraphQLResolveInfo } from 'graphql'; +export type Maybe = T | null | undefined; +export type InputMaybe = T | null | undefined; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +export type RequireFields = Omit & { [P in K]-?: NonNullable }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string; } + String: { input: string; output: string; } + Boolean: { input: boolean; output: boolean; } + Int: { input: number; output: number; } + Float: { input: number; output: number; } +}; + +export type Mutation = { + readonly __typename?: 'Mutation'; + readonly sendSms: Scalars['Boolean']['output']; +}; + + +export type MutationSendSmsArgs = { + from: Scalars['String']['input']; + message: Scalars['String']['input']; + to: Scalars['String']['input']; +}; + +export type WithIndex = TObject & Record; +export type ResolversObject = WithIndex; + +export type ResolverTypeWrapper = Promise | T; + + +export type ResolverWithResolve = { + resolve: ResolverFn; +}; +export type Resolver = ResolverFn | ResolverWithResolve; + +export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => Promise | TResult; + +export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => AsyncIterable | Promise>; + +export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + +export interface SubscriptionSubscriberObject { + subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; + resolve?: SubscriptionResolveFn; +} + +export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; +} + +export type SubscriptionObject = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + +export type SubscriptionResolver = + | ((...args: any[]) => SubscriptionObject) + | SubscriptionObject; + +export type TypeResolveFn = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo +) => Maybe | Promise>; + +export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; + +export type NextResolverFn = () => Promise; + +export type DirectiveResolverFn = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + + + +/** Mapping between all available schema types and the resolvers types */ +export type ResolversTypes = ResolversObject<{ + Boolean: ResolverTypeWrapper; + Mutation: ResolverTypeWrapper<{}>; + String: ResolverTypeWrapper; +}>; + +/** Mapping between all available schema types and the resolvers parents */ +export type ResolversParentTypes = ResolversObject<{ + Boolean: Scalars['Boolean']['output']; + Mutation: {}; + String: Scalars['String']['output']; +}>; + +export type MutationResolvers = ResolversObject<{ + sendSms?: Resolver>; +}>; + +export type Resolvers = ResolversObject<{ + Mutation?: MutationResolvers; +}>; + diff --git a/packages/sms-46elks/graphql/schema.graphql b/packages/sms-46elks/graphql/schema.graphql new file mode 100644 index 00000000..97011670 --- /dev/null +++ b/packages/sms-46elks/graphql/schema.graphql @@ -0,0 +1,7 @@ +type Mutation { + sendSms( + from: String!, + to: String!, + message: String!, + ): Boolean! +} diff --git a/packages/sms-46elks/package.json b/packages/sms-46elks/package.json new file mode 100644 index 00000000..86ea9848 --- /dev/null +++ b/packages/sms-46elks/package.json @@ -0,0 +1,49 @@ +{ + "name": "@zemble/sms-46elks", + "version": "0.0.1", + "description": "@zemble Plugin for sending sms via 46elks", + "main": "plugin.ts", + "scripts": { + "dev": "zemble-dev plugin.ts", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "codegen": "graphql-codegen" + }, + "keywords": [ + "zemble", + "zemble-plugin", + "@zemble" + ], + "author": { + "name": "Robert Herber", + "email": "robert@kingstinct.com", + "url": "https://github.com/robertherber" + }, + "repository": { + "type": "git", + "url": "https://github.com/kingstinct/zemble", + "directory": "packages/apple-app-site-association" + }, + "funding": [ + "https://github.com/sponsors/kingstinct", + "https://github.com/sponsors/robertherber" + ], + "bugs": { + "url": "https://github.com/kingstinct/zemble/issues" + }, + "license": "ISC", + "dependencies": { + "@zemble/core": "workspace:*", + "@zemble/graphql": "workspace:*", + "graphql": "^16.8.1" + }, + "devDependencies": { + "@graphql-codegen/add": "^5.0.2", + "@graphql-codegen/cli": "^5.0.2", + "@graphql-codegen/client-preset": "^4.2.5", + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/packages/sms-46elks/plugin.ts b/packages/sms-46elks/plugin.ts new file mode 100644 index 00000000..c7cfb43a --- /dev/null +++ b/packages/sms-46elks/plugin.ts @@ -0,0 +1,132 @@ +/* eslint-disable no-console */ +import { Plugin, setupProvider } from '@zemble/core' +import yoga from '@zemble/graphql' + +import type { IStandardSendSmsService } from '@zemble/core' + +/* + * This plugin is used to send SMS messages using the 46Elks API. + * Docs: https://46elks.se/docs/send-sms#:~:text=(response.text)-,Optional%20request%20parameters,message,-Enable%20to%20avoid + * + * To use this plugin, you need to set the following environment variables: + * + * - ELKS_USERNAME: The username for the 46Elks API + * - ELKS_PASSWORD: The password for the 46Elks API + * + * + * You can also set the following configuration options in the plugin configuration: + * + * - disable: If set to true, the plugin will be disabled + * - options: An object with the following options: + * - dryrun: If set to 'yes', the plugin will not send any SMS messages. + * - flashsms: Send the message as a Flash SMS. The message will be displayed immediately upon arrival and not + * stored. + * - dontlog: Enable to avoid storing the message text in your history. The other parameters will still be + * stored. + * - whendelivered: A webhook URL will receive a POST request every time the delivery status changes. +*/ +interface Sms46ElksConfig extends Zemble.GlobalConfig { + readonly ELKS_USERNAME?: string + readonly ELKS_PASSWORD?: string + readonly disable?: boolean + readonly options: { + readonly dryrun?: 'yes' | 'no' + readonly flashsms?: 'yes' | 'no' + readonly dontlog?: 'message' | 'from' | 'to' + readonly whendelivered?: string + } +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Zemble { + interface MiddlewareConfig { + readonly ['@zemble/sms-46elks']?: Zemble.DefaultMiddlewareConfig + } + + interface Providers { + // eslint-disable-next-line functional/prefer-readonly-type + sendSms: IStandardSendSmsService + } + } +} + +const defaultConfig = { + ELKS_USERNAME: process.env['ELKS_USERNAME'], + ELKS_PASSWORD: process.env['ELKS_PASSWORD'], + disable: process.env.NODE_ENV === 'test', + middleware: { + '@zemble/graphql': { + disable: true, + }, + }, +} satisfies Partial + +// eslint-disable-next-line unicorn/consistent-function-scoping +const plugin = new Plugin(import.meta.dir, { + middleware: async ({ + config, app, logger, + }) => { + if (!config.disable) { + // eslint-disable-next-line unicorn/consistent-function-scoping + const initializeProvider = (): IStandardSendSmsService => async (data) => { + if (!config.ELKS_USERNAME || !config.ELKS_PASSWORD) { + logger.warn('ELKS_USERNAME and ELKS_PASSWORD must be set to send sms, skipping') + return false + } + + const auth = Buffer.from(`${config.ELKS_USERNAME}:${config.ELKS_PASSWORD}`).toString('base64') + + let formatedData = { ...data, from: data.from.replace(/\s/g, '') } + + if (config.options) { + formatedData = { ...formatedData, ...config.options } + } + + const body = new URLSearchParams(formatedData).toString() + + try { + const response = await fetch('https://api.46elks.com/a1/sms', { + method: 'POST', + body, + headers: { Authorization: `Basic ${auth}` }, + }) + + const { ok } = response + + if (!ok) { + throw new Error(`Failed to send the SMS message. Error code: ${response.status}. Status: ${response.statusText}.`) + } + + return true + } catch (error) { + if (error instanceof Error) { + logger.error('Error sending sms', error.message) + } + return false + } + } + await setupProvider({ + app, + initializeProvider, + providerKey: 'sendSms', + middlewareKey: '@zemble/sms-46elks', + }) + } + }, + dependencies: [ + { + plugin: yoga, + }, + ], + defaultConfig, + additionalConfigWhenRunningLocally: { + middleware: { + '@zemble/graphql': { + disable: false, + }, + }, + }, +}) + +export default plugin diff --git a/packages/sms-46elks/tsconfig.json b/packages/sms-46elks/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/sms-46elks/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/packages/sms-twilio/.env.dev b/packages/sms-twilio/.env.dev new file mode 100644 index 00000000..134b71b3 --- /dev/null +++ b/packages/sms-twilio/.env.dev @@ -0,0 +1,67 @@ + +PUBLIC_KEY='-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArRjgBxq2WSsxHvXeM0Gu +fa1s1Ovoo3UQfd239dO+vxc8H4FbO60KYkVwSPwTfqOvre5pp5XkyFFhHonNNrQa +qAzPCihYhIwvL0H7CvOaQqDCDGt5+UZnQ1gHxng9zTDOP3NKtmZ6d9VDHydK+5tW +3sObf2aLXA19W1r4s++SzWtr1zkHs+NJ2L8m0Jm/mDu45n1pz4eKZK43iTwUcpRz +SPfreK8lzKulhP+Ehr+1o6Px3y5C4uHdl0FN6B6bdbR/OfQuCDQ+KBIeW13epEep +0W675WIUF7c7lCyzstskRPkB9SITYTjvzEesWarQo9C9vr22AGCF70JJbuGoDAk2 +TpPEA1S4bZWZxwdTTV6Ys9iQd4dSWh39G+rnt7I2QMJhHmGnYA/PSDGxyDd9y5ub +prKGX5UERdGAPEZxgJjCRFhOZgCsYXMewg246oUpMe8Efvi67DqZqPHJmJK2++Cz +nEhrZXqEvSSpVvnP89RXutLCvOdFeWxx4NlPpjwI0Whs+JPrpFuqXKhRX7cbk33X +zD79jNdVXZEWDCwOkENeDqfPM5KiQD3y6geDAFghBge9Xwb9FMPCJ9GV2Zt2cbFL +PmS5NlwkjP5O9jHswPFZdsPT5oud/zrNB5OYf2J1qyUl0y6tu7ryH697p9PJECXP +pduX76EzFL7Qe9r9jlUNCm8CAwEAAQ== +-----END PUBLIC KEY-----' +PRIVATE_KEY='-----BEGIN PRIVATE KEY----- +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 +DmxKOUbG7CHn8nB21sC7cvKKEIxoCg== +-----END PRIVATE KEY-----' \ No newline at end of file diff --git a/packages/sms-twilio/.env.test b/packages/sms-twilio/.env.test new file mode 100644 index 00000000..134b71b3 --- /dev/null +++ b/packages/sms-twilio/.env.test @@ -0,0 +1,67 @@ + +PUBLIC_KEY='-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArRjgBxq2WSsxHvXeM0Gu +fa1s1Ovoo3UQfd239dO+vxc8H4FbO60KYkVwSPwTfqOvre5pp5XkyFFhHonNNrQa +qAzPCihYhIwvL0H7CvOaQqDCDGt5+UZnQ1gHxng9zTDOP3NKtmZ6d9VDHydK+5tW +3sObf2aLXA19W1r4s++SzWtr1zkHs+NJ2L8m0Jm/mDu45n1pz4eKZK43iTwUcpRz +SPfreK8lzKulhP+Ehr+1o6Px3y5C4uHdl0FN6B6bdbR/OfQuCDQ+KBIeW13epEep +0W675WIUF7c7lCyzstskRPkB9SITYTjvzEesWarQo9C9vr22AGCF70JJbuGoDAk2 +TpPEA1S4bZWZxwdTTV6Ys9iQd4dSWh39G+rnt7I2QMJhHmGnYA/PSDGxyDd9y5ub +prKGX5UERdGAPEZxgJjCRFhOZgCsYXMewg246oUpMe8Efvi67DqZqPHJmJK2++Cz +nEhrZXqEvSSpVvnP89RXutLCvOdFeWxx4NlPpjwI0Whs+JPrpFuqXKhRX7cbk33X +zD79jNdVXZEWDCwOkENeDqfPM5KiQD3y6geDAFghBge9Xwb9FMPCJ9GV2Zt2cbFL +PmS5NlwkjP5O9jHswPFZdsPT5oud/zrNB5OYf2J1qyUl0y6tu7ryH697p9PJECXP +pduX76EzFL7Qe9r9jlUNCm8CAwEAAQ== +-----END PUBLIC KEY-----' +PRIVATE_KEY='-----BEGIN PRIVATE KEY----- +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 +DmxKOUbG7CHn8nB21sC7cvKKEIxoCg== +-----END PRIVATE KEY-----' \ No newline at end of file diff --git a/packages/sms-twilio/.eslintrc b/packages/sms-twilio/.eslintrc new file mode 100644 index 00000000..5547e0ef --- /dev/null +++ b/packages/sms-twilio/.eslintrc @@ -0,0 +1,5 @@ +{ + "extends": [ + "../../.eslintrc" + ] +} diff --git a/packages/sms-twilio/.gitignore b/packages/sms-twilio/.gitignore new file mode 100644 index 00000000..e6905a23 --- /dev/null +++ b/packages/sms-twilio/.gitignore @@ -0,0 +1 @@ +.env* \ No newline at end of file diff --git a/packages/sms-twilio/README.md b/packages/sms-twilio/README.md new file mode 100644 index 00000000..d6ec1982 --- /dev/null +++ b/packages/sms-twilio/README.md @@ -0,0 +1,7 @@ +# @zemble/sms-twilio + +This package is a @zemble plugin to easily send SMS messages using the Twilio API. + +Read more about [Twilio](https://www.twilio.com/en-us) + +Read more about [@zemble](https://github.com/kingstinct/zemble) diff --git a/packages/sms-twilio/codegen.ts b/packages/sms-twilio/codegen.ts new file mode 100644 index 00000000..2153aff7 --- /dev/null +++ b/packages/sms-twilio/codegen.ts @@ -0,0 +1,7 @@ +import defaultConfig from '@zemble/graphql/codegen' + +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = defaultConfig + +export default config diff --git a/packages/sms-twilio/graphql/Mutation/sendSms.ts b/packages/sms-twilio/graphql/Mutation/sendSms.ts new file mode 100644 index 00000000..fee3e3b9 --- /dev/null +++ b/packages/sms-twilio/graphql/Mutation/sendSms.ts @@ -0,0 +1,7 @@ +import plugin from '../../plugin' + +import type { MutationResolvers } from '../schema.generated' + +export const sendSms: NonNullable = async (_, data) => plugin.providers.sendSms(data) + +export default sendSms diff --git a/packages/sms-twilio/graphql/schema.generated.ts b/packages/sms-twilio/graphql/schema.generated.ts new file mode 100644 index 00000000..c44cc402 --- /dev/null +++ b/packages/sms-twilio/graphql/schema.generated.ts @@ -0,0 +1,124 @@ +// @ts-nocheck +import '@zemble/core' +import type { GraphQLResolveInfo } from 'graphql'; +export type Maybe = T | null | undefined; +export type InputMaybe = T | null | undefined; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +export type RequireFields = Omit & { [P in K]-?: NonNullable }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string; } + String: { input: string; output: string; } + Boolean: { input: boolean; output: boolean; } + Int: { input: number; output: number; } + Float: { input: number; output: number; } +}; + +export type Mutation = { + readonly __typename?: 'Mutation'; + readonly sendSms: Scalars['Boolean']['output']; +}; + + +export type MutationSendSmsArgs = { + from: Scalars['String']['input']; + message: Scalars['String']['input']; + to: Scalars['String']['input']; +}; + +export type WithIndex = TObject & Record; +export type ResolversObject = WithIndex; + +export type ResolverTypeWrapper = Promise | T; + + +export type ResolverWithResolve = { + resolve: ResolverFn; +}; +export type Resolver = ResolverFn | ResolverWithResolve; + +export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => Promise | TResult; + +export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => AsyncIterable | Promise>; + +export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + +export interface SubscriptionSubscriberObject { + subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; + resolve?: SubscriptionResolveFn; +} + +export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; +} + +export type SubscriptionObject = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + +export type SubscriptionResolver = + | ((...args: any[]) => SubscriptionObject) + | SubscriptionObject; + +export type TypeResolveFn = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo +) => Maybe | Promise>; + +export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; + +export type NextResolverFn = () => Promise; + +export type DirectiveResolverFn = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + + + +/** Mapping between all available schema types and the resolvers types */ +export type ResolversTypes = ResolversObject<{ + Boolean: ResolverTypeWrapper; + Mutation: ResolverTypeWrapper<{}>; + String: ResolverTypeWrapper; +}>; + +/** Mapping between all available schema types and the resolvers parents */ +export type ResolversParentTypes = ResolversObject<{ + Boolean: Scalars['Boolean']['output']; + Mutation: {}; + String: Scalars['String']['output']; +}>; + +export type MutationResolvers = ResolversObject<{ + sendSms?: Resolver>; +}>; + +export type Resolvers = ResolversObject<{ + Mutation?: MutationResolvers; +}>; + diff --git a/packages/sms-twilio/graphql/schema.graphql b/packages/sms-twilio/graphql/schema.graphql new file mode 100644 index 00000000..97011670 --- /dev/null +++ b/packages/sms-twilio/graphql/schema.graphql @@ -0,0 +1,7 @@ +type Mutation { + sendSms( + from: String!, + to: String!, + message: String!, + ): Boolean! +} diff --git a/packages/sms-twilio/package.json b/packages/sms-twilio/package.json new file mode 100644 index 00000000..9c1fdae1 --- /dev/null +++ b/packages/sms-twilio/package.json @@ -0,0 +1,49 @@ +{ + "name": "@zemble/sms-twilio", + "version": "0.0.1", + "description": "@zemble Plugin for sending sms via Twilio", + "main": "plugin.ts", + "scripts": { + "dev": "zemble-dev plugin.ts", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "codegen": "graphql-codegen" + }, + "keywords": [ + "zemble", + "zemble-plugin", + "@zemble" + ], + "author": { + "name": "Robert Herber", + "email": "robert@kingstinct.com", + "url": "https://github.com/robertherber" + }, + "repository": { + "type": "git", + "url": "https://github.com/kingstinct/zemble", + "directory": "packages/apple-app-site-association" + }, + "funding": [ + "https://github.com/sponsors/kingstinct", + "https://github.com/sponsors/robertherber" + ], + "bugs": { + "url": "https://github.com/kingstinct/zemble/issues" + }, + "license": "ISC", + "dependencies": { + "@zemble/core": "workspace:*", + "@zemble/graphql": "workspace:*", + "graphql": "^16.8.1" + }, + "devDependencies": { + "@graphql-codegen/add": "^5.0.2", + "@graphql-codegen/cli": "^5.0.2", + "@graphql-codegen/client-preset": "^4.2.5", + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/packages/sms-twilio/plugin.ts b/packages/sms-twilio/plugin.ts new file mode 100644 index 00000000..07a969be --- /dev/null +++ b/packages/sms-twilio/plugin.ts @@ -0,0 +1,112 @@ +/* eslint-disable no-console */ +import { Plugin, setupProvider } from '@zemble/core' +import yoga from '@zemble/graphql' + +import type { IStandardSendSmsService } from '@zemble/core' + +interface SmsTwilioConfig extends Zemble.GlobalConfig { + readonly TWILIO_ACCOUNT_SID?: string + readonly TWILIO_AUTH_TOKEN?: string + readonly disable?: boolean +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Zemble { + interface MiddlewareConfig { + readonly ['@zemble/sms-twilio']?: Zemble.DefaultMiddlewareConfig + } + + interface Providers { + // eslint-disable-next-line functional/prefer-readonly-type + sendSms: IStandardSendSmsService + } + } +} + +const defaultConfig = { + TWILIO_ACCOUNT_SID: process.env['TWILIO_ACCOUNT_SID'], + TWILIO_AUTH_TOKEN: process.env['TWILIO_AUTH_TOKEN'], + disable: process.env.NODE_ENV === 'test', + middleware: { + '@zemble/graphql': { + disable: true, + }, + }, +} satisfies Partial + +// eslint-disable-next-line unicorn/consistent-function-scoping +const plugin = new Plugin(import.meta.dir, { + middleware: async ({ + config, app, logger, + }) => { + if (!config.disable) { + // eslint-disable-next-line unicorn/consistent-function-scoping + const initializeProvider = (): IStandardSendSmsService => async ({ to, message, from }) => { + const { + TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, + } = config + + if (!TWILIO_ACCOUNT_SID || !TWILIO_AUTH_TOKEN) { + logger.warn('Twilio account SID and AuthToken must be set to send sms, skipping') + return false + } + + const url = `https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json` + + const body = new URLSearchParams({ + From: from, + To: to, + Body: message, + }) + + const headers = { + 'Authorization': `Basic ${Buffer.from(`${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}`).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + } + + try { + const response = await fetch(url, { + method: 'POST', + headers, + body, + }) + + if (!response.ok) { + throw new Error(`Failed to send the SMS message. Error code: ${response.status}. Status: ${response.statusText}.`) + } + + return true + } catch (error) { + if (error instanceof Error) { + logger.error('Error sending sms', error.message) + } + + return false + } + } + + await setupProvider({ + app, + initializeProvider, + providerKey: 'sendSms', + middlewareKey: '@zemble/sms-twilio', + }) + } + }, + dependencies: [ + { + plugin: yoga, + }, + ], + defaultConfig, + additionalConfigWhenRunningLocally: { + middleware: { + '@zemble/graphql': { + disable: false, + }, + }, + }, +}) + +export default plugin diff --git a/packages/sms-twilio/tsconfig.json b/packages/sms-twilio/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/sms-twilio/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/turbo.json b/turbo.json index 12370444..5c4cb2fb 100644 --- a/turbo.json +++ b/turbo.json @@ -40,6 +40,7 @@ ] }, "codegen": { + "cache": false, "outputMode": "errors-only", "outputs": [ "**/*.generated/**/*",