From 9b3f6d745e781edf1051e9febf7dc1e2c9e8758e Mon Sep 17 00:00:00 2001 From: Dipendra Upreti <91712746+dipendraupreti@users.noreply.github.com> Date: Wed, 10 May 2023 18:08:40 +0545 Subject: [PATCH] feat: Multi tenant authentication (#355) --- packages/mercurius/src/buildContext.ts | 1 + packages/mercurius/src/index.ts | 1 + packages/multi-tenant/README.md | 18 ++- packages/multi-tenant/package.json | 10 +- packages/multi-tenant/src/index.ts | 2 + .../multi-tenant/src/lib/discoverTenant.ts | 5 +- ...enantConfig.ts => getMultiTenantConfig.ts} | 0 .../multi-tenant/src/lib/getUserService.ts | 31 +++++ .../multi-tenant/src/lib/updateContext.ts | 44 ++++++- packages/multi-tenant/src/migratePlugin.ts | 3 +- .../tenants/__test__/helpers/createConfig.ts | 2 +- .../multi-tenant/src/model/tenants/service.ts | 2 +- packages/multi-tenant/src/plugin.ts | 13 +++ .../src/supertokens/recipes/index.ts | 33 ++++++ .../emailPasswordSignIn.ts | 71 ++++++++++++ .../emailPasswordSignInPost.ts | 20 ++++ .../emailPasswordSignUp.ts | 109 ++++++++++++++++++ .../emailPasswordSignUpPost.ts | 20 ++++ .../generatePasswordResetTokenPost.ts | 30 +++++ .../third-party-email-password/getUserById.ts | 22 ++++ .../third-party-email-password/index.ts | 9 ++ .../third-party-email-password/sendEmail.ts | 44 +++++++ .../thirdPartySignInUp.ts | 58 ++++++++++ .../thirdPartySignInUpPost.ts | 103 +++++++++++++++++ .../src/supertokens/utils/email.ts | 28 +++++ .../src/supertokens/utils/getOrigin.ts | 17 +++ .../src/supertokens/utils/sendEmail.ts | 41 +++++++ .../src/supertokens/utils/updateFields.ts | 30 +++++ .../multi-tenant/src/tenantDiscoveryPlugin.ts | 7 +- packages/multi-tenant/vite.config.ts | 11 ++ packages/slonik/src/index.ts | 1 + packages/slonik/src/plugin.ts | 2 + .../user/src/__test__/helpers/createConfig.ts | 5 +- packages/user/src/index.ts | 8 +- packages/user/src/model/users/controller.ts | 70 +---------- .../model/users/handlers/changePassword.ts | 41 +++++++ .../user/src/model/users/handlers/index.ts | 5 + packages/user/src/model/users/handlers/me.ts | 20 ++++ .../user/src/model/users/handlers/users.ts | 26 +++++ packages/user/src/model/users/resolver.ts | 29 ++++- packages/user/src/types/index.ts | 14 +++ packages/user/src/userContext.ts | 6 +- pnpm-lock.yaml | 31 ++++- 43 files changed, 948 insertions(+), 95 deletions(-) rename packages/multi-tenant/src/lib/{multiTenantConfig.ts => getMultiTenantConfig.ts} (100%) create mode 100644 packages/multi-tenant/src/lib/getUserService.ts create mode 100644 packages/multi-tenant/src/supertokens/recipes/index.ts create mode 100644 packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignIn.ts create mode 100644 packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignInPost.ts create mode 100644 packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignUp.ts create mode 100644 packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignUpPost.ts create mode 100644 packages/multi-tenant/src/supertokens/recipes/third-party-email-password/generatePasswordResetTokenPost.ts create mode 100644 packages/multi-tenant/src/supertokens/recipes/third-party-email-password/getUserById.ts create mode 100644 packages/multi-tenant/src/supertokens/recipes/third-party-email-password/index.ts create mode 100644 packages/multi-tenant/src/supertokens/recipes/third-party-email-password/sendEmail.ts create mode 100644 packages/multi-tenant/src/supertokens/recipes/third-party-email-password/thirdPartySignInUp.ts create mode 100644 packages/multi-tenant/src/supertokens/recipes/third-party-email-password/thirdPartySignInUpPost.ts create mode 100644 packages/multi-tenant/src/supertokens/utils/email.ts create mode 100644 packages/multi-tenant/src/supertokens/utils/getOrigin.ts create mode 100644 packages/multi-tenant/src/supertokens/utils/sendEmail.ts create mode 100644 packages/multi-tenant/src/supertokens/utils/updateFields.ts create mode 100644 packages/user/src/model/users/handlers/changePassword.ts create mode 100644 packages/user/src/model/users/handlers/index.ts create mode 100644 packages/user/src/model/users/handlers/me.ts create mode 100644 packages/user/src/model/users/handlers/users.ts diff --git a/packages/mercurius/src/buildContext.ts b/packages/mercurius/src/buildContext.ts index 5ea6fa11f..2031b6c49 100644 --- a/packages/mercurius/src/buildContext.ts +++ b/packages/mercurius/src/buildContext.ts @@ -7,6 +7,7 @@ const buildContext = async (request: FastifyRequest, reply: FastifyReply) => { const context = { config: request.config, database: request.slonik, + dbSchema: request.dbSchema, } as MercuriusContext; if (plugins) { diff --git a/packages/mercurius/src/index.ts b/packages/mercurius/src/index.ts index 1fbac21cd..5a51d4939 100644 --- a/packages/mercurius/src/index.ts +++ b/packages/mercurius/src/index.ts @@ -7,6 +7,7 @@ declare module "mercurius" { interface MercuriusContext { config: ApiConfig; database: Database; + dbSchema: string; } } diff --git a/packages/multi-tenant/README.md b/packages/multi-tenant/README.md index 778896a42..ad05e7b8f 100644 --- a/packages/multi-tenant/README.md +++ b/packages/multi-tenant/README.md @@ -8,7 +8,10 @@ When registered on a Fastify instance, the plugin will: ## Requirements * `@dzangolab/fastify-config` +* `@dzangolab/fastify-mailer` +* `@dzangolab/fastify-mercurius` * `@dzangolab/fastify-slonik` +* `@dzangolab/fastify-user` ## Tenants table @@ -30,13 +33,13 @@ The table should contain the following columns: In a simple repo: ```bash -npm install @dzangolab/fastify-config @dzangolab/fastify-slonik @dzangolab/fastify-multi-tenant +npm install @dzangolab/fastify-config @dzangolab/fastify-mailer @dzangolab/fastify-mercurius @dzangolab/fastify-slonik @dzangolab/fastify-multi-tenant @dzangolab/fastify-user ``` If using in a monorepo with pnpm: ```bash -pnpm add --filter "myrepo" @dzangolab/fastify-config @dzangolab/fastify-slonik @dzangolab/fastify-multi-tenant +pnpm add --filter "myrepo" @dzangolab/fastify-config @dzangolab/fastify-mailer @dzangolab/fastify-mercurius @dzangolab/fastify-slonik @dzangolab/fastify-multi-tenant @dzangolab/fastify-user ``` ## Usage @@ -62,11 +65,20 @@ const fastify = Fastify({ }); // Register fastify-config plugin -fastify.register(configPlugin, { config }); +await fastify.register(configPlugin, { config }); + +// Register mailer plugin +await api.register(mailerPlugin); // Register database plugin await api.register(slonikPlugin); +// Register mercurius plugin +await api.register(mercuriusPlugin); + +// Register user plugin +await api.register(userPlugin); + await fastify.register(multiTenantPlugin); await fastify.listen({ diff --git a/packages/multi-tenant/package.json b/packages/multi-tenant/package.json index 801295fe7..262009d99 100644 --- a/packages/multi-tenant/package.json +++ b/packages/multi-tenant/package.json @@ -33,13 +33,17 @@ "dependencies": { "@dzangolab/postgres-migrations": "5.4.1", "humps": "2.0.1", - "pg": "8.8.0" + "pg": "8.8.0", + "lodash.merge": "4.6.2" }, "devDependencies": { "@dzangolab/fastify-config": "0.31.3", + "@dzangolab/fastify-mailer": "0.31.3", "@dzangolab/fastify-mercurius": "0.31.3", "@dzangolab/fastify-slonik": "0.31.3", + "@dzangolab/fastify-user": "0.31.3", "@types/humps": "2.0.2", + "@types/lodash.merge": "4.6.7", "@types/node": "18.15.11", "@types/pg": "8.6.6", "@typescript-eslint/eslint-plugin": "5.59.2", @@ -52,6 +56,7 @@ "mercurius": "12.2.0", "prettier": "2.8.8", "slonik": "33.1.4", + "supertokens-node": "12.1.6", "tsconfig": "0.31.3", "typescript": "4.9.5", "vite": "4.3.5", @@ -61,12 +66,15 @@ }, "peerDependencies": { "@dzangolab/fastify-config": "0.31.3", + "@dzangolab/fastify-mailer": "0.31.3", "@dzangolab/fastify-mercurius": "0.31.3", "@dzangolab/fastify-slonik": "0.31.3", + "@dzangolab/fastify-user": "0.31.3", "fastify": ">=4.9.2", "fastify-plugin": ">=4.3.0", "mercurius": ">=12.2.0", "slonik": ">=33.1.4", + "supertokens-node": ">=12.1.6", "zod": ">=3.21.4" }, "engines": { diff --git a/packages/multi-tenant/src/index.ts b/packages/multi-tenant/src/index.ts index 814a71017..3ad2ba7fa 100644 --- a/packages/multi-tenant/src/index.ts +++ b/packages/multi-tenant/src/index.ts @@ -25,6 +25,8 @@ export { default } from "./plugin"; export { default as TenantService } from "./model/tenants/service"; +export { default as thirdPartyEmailPassword } from "./supertokens/recipes"; + export type { MultiTenantConfig, Tenant, diff --git a/packages/multi-tenant/src/lib/discoverTenant.ts b/packages/multi-tenant/src/lib/discoverTenant.ts index c735d059b..90273060d 100644 --- a/packages/multi-tenant/src/lib/discoverTenant.ts +++ b/packages/multi-tenant/src/lib/discoverTenant.ts @@ -2,13 +2,14 @@ import { ApiConfig } from "@dzangolab/fastify-config"; import TenantService from "../model/tenants/service"; +import type { Tenant } from "../types"; import type { Database } from "@dzangolab/fastify-slonik"; const discoverTenant = async ( config: ApiConfig, database: Database, host: string -) => { +): Promise => { const reservedSlugs = config.multiTenant?.reserved?.slugs; const reservedDomains = config.multiTenant?.reserved?.domains; @@ -32,7 +33,7 @@ const discoverTenant = async ( const tenant = await tenantService.findByHostname(host); if (tenant) { - return tenant; + return tenant as Tenant; } throw new Error("Tenant not found"); diff --git a/packages/multi-tenant/src/lib/multiTenantConfig.ts b/packages/multi-tenant/src/lib/getMultiTenantConfig.ts similarity index 100% rename from packages/multi-tenant/src/lib/multiTenantConfig.ts rename to packages/multi-tenant/src/lib/getMultiTenantConfig.ts diff --git a/packages/multi-tenant/src/lib/getUserService.ts b/packages/multi-tenant/src/lib/getUserService.ts new file mode 100644 index 000000000..6fa549f62 --- /dev/null +++ b/packages/multi-tenant/src/lib/getUserService.ts @@ -0,0 +1,31 @@ +import { UserService } from "@dzangolab/fastify-user"; + +import getMultiTenantConfig from "./getMultiTenantConfig"; + +import type { Tenant } from "../types/tenant"; +import type { ApiConfig } from "@dzangolab/fastify-config"; +import type { Database } from "@dzangolab/fastify-slonik"; +import type { + User, + UserCreateInput, + UserUpdateInput, +} from "@dzangolab/fastify-user"; +import type { QueryResultRow } from "slonik"; + +const getUserService = ( + config: ApiConfig, + slonik: Database, + tenant?: Tenant +) => { + const multiTenantConfig = getMultiTenantConfig(config); + + const dbSchema = tenant ? tenant[multiTenantConfig.table.columns.slug] : ""; + + return new UserService< + User & QueryResultRow, + UserCreateInput, + UserUpdateInput + >(config, slonik, dbSchema); +}; + +export default getUserService; diff --git a/packages/multi-tenant/src/lib/updateContext.ts b/packages/multi-tenant/src/lib/updateContext.ts index 05609938c..c150c66fc 100644 --- a/packages/multi-tenant/src/lib/updateContext.ts +++ b/packages/multi-tenant/src/lib/updateContext.ts @@ -1,12 +1,48 @@ -import type { FastifyRequest } from "fastify"; +import { wrapResponse } from "supertokens-node/framework/fastify"; +import Session from "supertokens-node/recipe/session"; +import UserRoles from "supertokens-node/recipe/userroles"; + +import getUserService from "../lib/getUserService"; + +import type { FastifyRequest, FastifyReply } from "fastify"; import type { MercuriusContext } from "mercurius"; const updateContext = async ( context: MercuriusContext, - request: FastifyRequest + request: FastifyRequest, + reply: FastifyReply ) => { - if (request.config.mercurius.enabled) { - context.tenant = request.tenant; + const { config, slonik, tenant } = request; + + context.tenant = tenant; + + const session = await Session.getSession(request, wrapResponse(reply), { + sessionRequired: false, + }); + + const userId = session?.getUserId(); + + if (userId && !context.user) { + const service = getUserService(config, slonik, tenant); + + /* eslint-disable-next-line unicorn/no-null */ + let user; + + try { + user = await service.findById(userId); + } catch { + // FIXME [OP 2022-AUG-22] Handle error properly + // DataIntegrityError + } + + if (!user) { + throw new Error("Unable to find user"); + } + + const { roles } = await UserRoles.getRolesForUser(userId); + + context.user = user; + context.roles = roles; } }; diff --git a/packages/multi-tenant/src/migratePlugin.ts b/packages/multi-tenant/src/migratePlugin.ts index 0f4475193..4231fbc18 100644 --- a/packages/multi-tenant/src/migratePlugin.ts +++ b/packages/multi-tenant/src/migratePlugin.ts @@ -4,8 +4,8 @@ import FastifyPlugin from "fastify-plugin"; import changeSchema from "./lib/changeSchema"; import getDatabaseConfig from "./lib/getDatabaseConfig"; +import getMultiTenantConfig from "./lib/getMultiTenantConfig"; import initializePgPool from "./lib/initializePgPool"; -import getMultiTenantConfig from "./lib/multiTenantConfig"; import runMigrations from "./lib/runMigrations"; import Service from "./model/tenants/service"; @@ -51,6 +51,7 @@ const plugin = async ( ); } } catch (error: unknown) { + /* eslint-disable-next-line unicorn/consistent-destructuring */ fastify.log.error("🔴 multi-tenant: Failed to run tenant migrations"); throw error; } diff --git a/packages/multi-tenant/src/model/tenants/__test__/helpers/createConfig.ts b/packages/multi-tenant/src/model/tenants/__test__/helpers/createConfig.ts index 70e5c6ea1..629fdc6be 100644 --- a/packages/multi-tenant/src/model/tenants/__test__/helpers/createConfig.ts +++ b/packages/multi-tenant/src/model/tenants/__test__/helpers/createConfig.ts @@ -37,7 +37,7 @@ const createConfig = (multiTenantConfig: Partial) => { }, }, version: "0.1", - }; + } as ApiConfig; return config; }; diff --git a/packages/multi-tenant/src/model/tenants/service.ts b/packages/multi-tenant/src/model/tenants/service.ts index 39fbf1fa5..a216e0625 100644 --- a/packages/multi-tenant/src/model/tenants/service.ts +++ b/packages/multi-tenant/src/model/tenants/service.ts @@ -1,8 +1,8 @@ import { BaseService } from "@dzangolab/fastify-slonik"; +import getMultiTenantConfig from "./../../lib/getMultiTenantConfig"; import SqlFactory from "./sqlFactory"; import getDatabaseConfig from "../../lib/getDatabaseConfig"; -import getMultiTenantConfig from "../../lib/multiTenantConfig"; import runMigrations from "../../lib/runMigrations"; import type { Tenant as BaseTenant } from "../../types"; diff --git a/packages/multi-tenant/src/plugin.ts b/packages/multi-tenant/src/plugin.ts index 84ad05b7f..4d28cb91b 100644 --- a/packages/multi-tenant/src/plugin.ts +++ b/packages/multi-tenant/src/plugin.ts @@ -1,7 +1,9 @@ import FastifyPlugin from "fastify-plugin"; +import merge from "lodash.merge"; import updateContext from "./lib/updateContext"; import migratePlugin from "./migratePlugin"; +import thirdPartyEmailPasswordConfig from "./supertokens/recipes"; import tenantDiscoveryPlugin from "./tenantDiscoveryPlugin"; import type { MercuriusEnabledPlugin } from "@dzangolab/fastify-mercurius"; @@ -20,6 +22,17 @@ const plugin = async ( // Register domain discovery plugin await fastify.register(tenantDiscoveryPlugin); + const { config } = fastify; + + const supertokensConfig = { + recipes: { + thirdPartyEmailPassword: thirdPartyEmailPasswordConfig, + }, + }; + + // merge supertokens config + config.user.supertokens = merge(supertokensConfig, config.user.supertokens); + done(); }; diff --git a/packages/multi-tenant/src/supertokens/recipes/index.ts b/packages/multi-tenant/src/supertokens/recipes/index.ts new file mode 100644 index 000000000..11d6357cd --- /dev/null +++ b/packages/multi-tenant/src/supertokens/recipes/index.ts @@ -0,0 +1,33 @@ +import { + emailPasswordSignIn, + emailPasswordSignUp, + emailPasswordSignUpPOST, + thirdPartySignInUp, + thirdPartySignInUpPOST, + sendEmail, + emailPasswordSignInPOST, + generatePasswordResetTokenPOST, + getUserById, +} from "./third-party-email-password"; + +import type { ThirdPartyEmailPasswordRecipe } from "@dzangolab/fastify-user"; + +const thirdPartyEmailPasswordConfig: ThirdPartyEmailPasswordRecipe = { + override: { + apis: { + emailPasswordSignInPOST, + emailPasswordSignUpPOST, + generatePasswordResetTokenPOST, + thirdPartySignInUpPOST, + }, + functions: { + emailPasswordSignIn, + emailPasswordSignUp, + getUserById, + thirdPartySignInUp, + }, + }, + sendEmail, +}; + +export default thirdPartyEmailPasswordConfig; diff --git a/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignIn.ts b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignIn.ts new file mode 100644 index 000000000..754c05fdc --- /dev/null +++ b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignIn.ts @@ -0,0 +1,71 @@ +import { formatDate } from "@dzangolab/fastify-user"; + +import getUserService from "../../../lib/getUserService"; +import Email from "../../utils/email"; + +import type { AuthUser } from "@dzangolab/fastify-user"; +import type { FastifyInstance } from "fastify"; +import type { RecipeInterface } from "supertokens-node/recipe/thirdpartyemailpassword"; + +const emailPasswordSignIn = ( + originalImplementation: RecipeInterface, + fastify: FastifyInstance +): RecipeInterface["emailPasswordSignIn"] => { + const { config, log, slonik } = fastify; + + return async (input) => { + input.email = Email.addTenantPrefix( + config, + input.email, + input.userContext.tenant + ); + + const originalResponse = await originalImplementation.emailPasswordSignIn( + input + ); + + if (originalResponse.status !== "OK") { + return originalResponse; + } + + const userService = getUserService( + config, + slonik, + input.userContext.tenant + ); + + const user = await userService.findById(originalResponse.user.id); + + if (!user) { + log.error(`User record not found for userId ${originalResponse.user.id}`); + + return { status: "WRONG_CREDENTIALS_ERROR" }; + } + + user.lastLoginAt = Date.now(); + + await userService + .update(user.id, { + lastLoginAt: formatDate(new Date(user.lastLoginAt)), + }) + /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ + .catch((error: any) => { + log.error( + `Unable to update lastLoginAt for userId ${originalResponse.user.id}` + ); + log.error(error); + }); + + const authUser: AuthUser = { + ...originalResponse.user, + ...user, + }; + + return { + status: "OK", + user: authUser, + }; + }; +}; + +export default emailPasswordSignIn; diff --git a/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignInPost.ts b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignInPost.ts new file mode 100644 index 000000000..8ab65a5ee --- /dev/null +++ b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignInPost.ts @@ -0,0 +1,20 @@ +import type { FastifyInstance } from "fastify"; +import type { APIInterface } from "supertokens-node/recipe/thirdpartyemailpassword/types"; + +const emailPasswordSignInPOST = ( + originalImplementation: APIInterface, + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + fastify: FastifyInstance +): APIInterface["emailPasswordSignInPOST"] => { + return async (input) => { + input.userContext.tenant = input.options.req.original.tenant; + + if (originalImplementation.emailPasswordSignInPOST === undefined) { + throw new Error("Should never come here"); + } + + return await originalImplementation.emailPasswordSignInPOST(input); + }; +}; + +export default emailPasswordSignInPOST; diff --git a/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignUp.ts b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignUp.ts new file mode 100644 index 000000000..51ebb79c9 --- /dev/null +++ b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignUp.ts @@ -0,0 +1,109 @@ +import { deleteUser } from "supertokens-node"; +import UserRoles from "supertokens-node/recipe/userroles"; + +import getUserService from "../../../lib/getUserService"; +import Email from "../../utils/email"; +import sendEmail from "../../utils/sendEmail"; + +import type { User } from "@dzangolab/fastify-user"; +import type { FastifyInstance, FastifyError } from "fastify"; +import type { RecipeInterface } from "supertokens-node/recipe/thirdpartyemailpassword"; + +const emailPasswordSignUp = ( + originalImplementation: RecipeInterface, + fastify: FastifyInstance +): RecipeInterface["emailPasswordSignUp"] => { + const { config, log, slonik } = fastify; + + return async (input) => { + if (config.user.features?.signUp === false) { + throw { + name: "SIGN_UP_DISABLED", + message: "SignUp feature is currently disabled", + statusCode: 404, + } as FastifyError; + } + + const originalEmail = input.email; + + input.email = Email.addTenantPrefix( + config, + originalEmail, + input.userContext.tenant + ); + + const originalResponse = await originalImplementation.emailPasswordSignUp( + input + ); + + if (originalResponse.status === "OK") { + const userService = getUserService( + config, + slonik, + input.userContext.tenant + ); + + let user: User | null | undefined; + + try { + user = await userService.create({ + id: originalResponse.user.id, + email: originalEmail, + }); + + if (!user) { + throw new Error("User not found"); + } + /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + log.error("Error while creating user"); + log.error(error); + + await deleteUser(originalResponse.user.id); + + throw { + name: "SIGN_UP_FAILED", + message: "Something went wrong", + statusCode: 500, + }; + } + + originalResponse.user = { + ...originalResponse.user, + ...user, + }; + + const rolesResponse = await UserRoles.addRoleToUser( + originalResponse.user.id, + config.user.role || "USER" + ); + + if (rolesResponse.status !== "OK") { + log.error(rolesResponse.status); + } + } + + if ( + config.user.supertokens.sendUserAlreadyExistsWarning && + originalResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" + ) { + try { + await sendEmail({ + fastify, + subject: "Duplicate Email Registration", + templateData: { + emailId: originalEmail, + }, + templateName: "duplicate-email-warning", + to: originalEmail, + }); + } catch (error) { + log.error(error); + } + } + + return originalResponse; + }; +}; + +export default emailPasswordSignUp; diff --git a/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignUpPost.ts b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignUpPost.ts new file mode 100644 index 000000000..e06f07875 --- /dev/null +++ b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/emailPasswordSignUpPost.ts @@ -0,0 +1,20 @@ +import type { FastifyInstance } from "fastify"; +import type { APIInterface } from "supertokens-node/recipe/thirdpartyemailpassword/types"; + +const emailPasswordSignUpPOST = ( + originalImplementation: APIInterface, + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + fastify: FastifyInstance +): APIInterface["emailPasswordSignUpPOST"] => { + return async (input) => { + input.userContext.tenant = input.options.req.original.tenant; + + if (originalImplementation.emailPasswordSignUpPOST === undefined) { + throw new Error("Should never come here"); + } + + return await originalImplementation.emailPasswordSignUpPOST(input); + }; +}; + +export default emailPasswordSignUpPOST; diff --git a/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/generatePasswordResetTokenPost.ts b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/generatePasswordResetTokenPost.ts new file mode 100644 index 000000000..85d589d40 --- /dev/null +++ b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/generatePasswordResetTokenPost.ts @@ -0,0 +1,30 @@ +import updateFields from "../../utils/updateFields"; + +import type { FastifyInstance } from "fastify"; +import type { APIInterface } from "supertokens-node/recipe/thirdpartyemailpassword/types"; + +const generatePasswordResetTokenPOST = ( + originalImplementation: APIInterface, + fastify: FastifyInstance +): APIInterface["generatePasswordResetTokenPOST"] => { + return async (input) => { + input.userContext.tenant = input.options.req.original.tenant; + + if (originalImplementation.generatePasswordResetTokenPOST === undefined) { + throw new Error("Should never come here"); + } + + input.formFields = updateFields( + fastify.config, + input.formFields, + input.userContext.tenant + ); + + const originalResponse = + await originalImplementation.generatePasswordResetTokenPOST(input); + + return originalResponse; + }; +}; + +export default generatePasswordResetTokenPOST; diff --git a/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/getUserById.ts b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/getUserById.ts new file mode 100644 index 000000000..2c85c304a --- /dev/null +++ b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/getUserById.ts @@ -0,0 +1,22 @@ +import Email from "../../utils/email"; + +import type { RecipeInterface } from "supertokens-node/recipe/thirdpartyemailpassword"; + +const getUserById = ( + originalImplementation: RecipeInterface +): RecipeInterface["getUserById"] => { + return async (input) => { + let user = await originalImplementation.getUserById(input); + + if (user && input.userContext.tenant) { + user = { + ...user, + email: Email.removeTenantPrefix(user.email, input.userContext.tenant), + }; + } + + return user; + }; +}; + +export default getUserById; diff --git a/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/index.ts b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/index.ts new file mode 100644 index 000000000..b83c7fd81 --- /dev/null +++ b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/index.ts @@ -0,0 +1,9 @@ +export { default as emailPasswordSignIn } from "./emailPasswordSignIn"; +export { default as emailPasswordSignInPOST } from "./emailPasswordSignInPost"; +export { default as emailPasswordSignUp } from "./emailPasswordSignUp"; +export { default as emailPasswordSignUpPOST } from "./emailPasswordSignUpPost"; +export { default as generatePasswordResetTokenPOST } from "./generatePasswordResetTokenPost"; +export { default as getUserById } from "./getUserById"; +export { default as sendEmail } from "./sendEmail"; +export { default as thirdPartySignInUp } from "./thirdPartySignInUp"; +export { default as thirdPartySignInUpPOST } from "./thirdPartySignInUpPost"; diff --git a/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/sendEmail.ts b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/sendEmail.ts new file mode 100644 index 000000000..863e4eef0 --- /dev/null +++ b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/sendEmail.ts @@ -0,0 +1,44 @@ +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"; + +import Email from "../../utils/email"; +import getOrigin from "../../utils/getOrigin"; +import mailer from "../../utils/sendEmail"; + +import type { FastifyInstance, FastifyRequest } from "fastify"; +import type { EmailDeliveryInterface } from "supertokens-node/lib/build/ingredients/emaildelivery/types"; +import type { TypeEmailPasswordPasswordResetEmailDeliveryInput } from "supertokens-node/lib/build/recipe/emailpassword/types"; + +const sendEmail = ( + originalImplementation: EmailDeliveryInterface, + fastify: FastifyInstance +): typeof ThirdPartyEmailPassword.sendEmail => { + const websiteDomain = fastify.config.appOrigin[0] as string; + const resetPasswordPath = "/reset-password"; + + return async (input) => { + const request: FastifyRequest = input.userContext._default.request.request; + + const url = + request.headers.referer || request.headers.origin || request.hostname; + + const origin = getOrigin(url) || websiteDomain; + + const passwordResetLink = input.passwordResetLink.replace( + websiteDomain + "/auth/reset-password", + origin + + (fastify.config.user.supertokens.resetPasswordPath || resetPasswordPath) + ); + + await mailer({ + fastify, + subject: "Reset Password", + templateName: "reset-password", + to: Email.removeTenantPrefix(input.user.email, input.userContext.tenant), + templateData: { + passwordResetLink, + }, + }); + }; +}; + +export default sendEmail; diff --git a/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/thirdPartySignInUp.ts b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/thirdPartySignInUp.ts new file mode 100644 index 000000000..f6fe4b656 --- /dev/null +++ b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/thirdPartySignInUp.ts @@ -0,0 +1,58 @@ +import { getUserByThirdPartyInfo } from "supertokens-node/recipe/thirdpartyemailpassword"; +import UserRoles from "supertokens-node/recipe/userroles"; + +import getMultiTenantConfig from "../../../lib/getMultiTenantConfig"; + +import type { Tenant } from "../../../types"; +import type { FastifyInstance, FastifyError } from "fastify"; +import type { RecipeInterface } from "supertokens-node/recipe/thirdpartyemailpassword"; + +const thirdPartySignInUp = ( + originalImplementation: RecipeInterface, + fastify: FastifyInstance +): RecipeInterface["thirdPartySignInUp"] => { + const { config, log } = fastify; + + return async (input) => { + const tenant: Tenant | undefined = input.userContext.tenant; + + if (tenant) { + const tenantId = tenant[getMultiTenantConfig(config).table.columns.id]; + + input.thirdPartyUserId = tenantId + "_" + input.thirdPartyUserId; + } + + const thirdPartyUser = await getUserByThirdPartyInfo( + input.thirdPartyId, + input.thirdPartyUserId, + input.userContext + ); + + if (!thirdPartyUser && config.user.features?.signUp === false) { + throw { + name: "SIGN_UP_DISABLED", + message: "SignUp feature is currently disabled", + statusCode: 404, + } as FastifyError; + } + + const originalResponse = await originalImplementation.thirdPartySignInUp( + input + ); + + if (originalResponse.status === "OK" && originalResponse.createdNewUser) { + const rolesResponse = await UserRoles.addRoleToUser( + originalResponse.user.id, + config.user.role || "USER" + ); + + if (rolesResponse.status !== "OK") { + log.error(rolesResponse.status); + } + } + + return originalResponse; + }; +}; + +export default thirdPartySignInUp; diff --git a/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/thirdPartySignInUpPost.ts b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/thirdPartySignInUpPost.ts new file mode 100644 index 000000000..f448cc6f4 --- /dev/null +++ b/packages/multi-tenant/src/supertokens/recipes/third-party-email-password/thirdPartySignInUpPost.ts @@ -0,0 +1,103 @@ +import { formatDate } from "@dzangolab/fastify-user"; +import { deleteUser } from "supertokens-node"; + +import getUserService from "../../../lib/getUserService"; + +import type { User } from "@dzangolab/fastify-user"; +import type { FastifyInstance } from "fastify"; +import type { APIInterface } from "supertokens-node/recipe/thirdpartyemailpassword/types"; + +const thirdPartySignInUpPOST = ( + originalImplementation: APIInterface, + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + fastify: FastifyInstance +): APIInterface["thirdPartySignInUpPOST"] => { + const { config, log, slonik } = fastify; + + return async (input) => { + input.userContext.tenant = input.options.req.original.tenant; + + if (originalImplementation.thirdPartySignInUpPOST === undefined) { + throw new Error("Should never come here"); + } + + const originalResponse = + await originalImplementation.thirdPartySignInUpPOST(input); + + if (originalResponse.status === "OK") { + const userService = getUserService( + config, + slonik, + input.userContext.tenant + ); + + let user: User | undefined | null; + + if (originalResponse.createdNewUser) { + try { + user = await userService.create({ + id: originalResponse.user.id, + email: originalResponse.user.email, + }); + + if (!user) { + throw new Error("User not found"); + } + /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + log.error("Error while creating user"); + log.error(error); + + await deleteUser(originalResponse.user.id); + + throw { + name: "SIGN_UP_FAILED", + message: "Something went wrong", + statusCode: 500, + }; + } + } else { + user = await userService.findById(originalResponse.user.id); + + if (!user) { + log.error( + `User record not found for userId ${originalResponse.user.id}` + ); + + return { + status: "GENERAL_ERROR", + message: "Something went wrong", + }; + } + + user.lastLoginAt = Date.now(); + + await userService + .update(user.id, { + lastLoginAt: formatDate(new Date(user.lastLoginAt)), + }) + /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ + .catch((error: any) => { + log.error( + `Unable to update lastLoginAt for userId ${originalResponse.user.id}` + ); + log.error(error); + }); + } + return { + status: "OK", + createdNewUser: originalResponse.createdNewUser, + user: { + ...originalResponse.user, + ...user, + }, + session: originalResponse.session, + authCodeResponse: originalResponse.authCodeResponse, + }; + } + + return originalResponse; + }; +}; + +export default thirdPartySignInUpPOST; diff --git a/packages/multi-tenant/src/supertokens/utils/email.ts b/packages/multi-tenant/src/supertokens/utils/email.ts new file mode 100644 index 000000000..9b257ed20 --- /dev/null +++ b/packages/multi-tenant/src/supertokens/utils/email.ts @@ -0,0 +1,28 @@ +import getMultiTenantConfig from "../../lib/getMultiTenantConfig"; + +import type { Tenant } from "../../types"; +import type { ApiConfig } from "@dzangolab/fastify-config"; + +const Email = { + addTenantPrefix: ( + config: ApiConfig, + email: string, + tenant: Tenant | undefined + ) => { + if (tenant) { + email = + tenant[getMultiTenantConfig(config).table.columns.id] + "_" + email; + } + + return email; + }, + removeTenantPrefix: (email: string, tenant: Tenant | undefined) => { + if (tenant) { + email = email.slice(Math.max(0, email.indexOf("_") + 1)); + } + + return email; + }, +}; + +export default Email; diff --git a/packages/multi-tenant/src/supertokens/utils/getOrigin.ts b/packages/multi-tenant/src/supertokens/utils/getOrigin.ts new file mode 100644 index 000000000..223aad8ca --- /dev/null +++ b/packages/multi-tenant/src/supertokens/utils/getOrigin.ts @@ -0,0 +1,17 @@ +const getOrigin = (url: string) => { + let origin: string; + + try { + origin = new URL(url).origin; + + if (!origin || origin === "null") { + throw new Error("Origin is empty"); + } + } catch { + origin = ""; + } + + return origin; +}; + +export default getOrigin; diff --git a/packages/multi-tenant/src/supertokens/utils/sendEmail.ts b/packages/multi-tenant/src/supertokens/utils/sendEmail.ts new file mode 100644 index 000000000..21acac50d --- /dev/null +++ b/packages/multi-tenant/src/supertokens/utils/sendEmail.ts @@ -0,0 +1,41 @@ +import "@dzangolab/fastify-mailer"; + +import type { FastifyInstance } from "fastify"; + +const sendEmail = async ({ + fastify, + subject, + templateData = {}, + templateName, + to, +}: { + fastify: FastifyInstance; + subject: string; + templateData?: Record; + templateName: string; + to: string; +}) => { + const { config, mailer, log } = fastify; + + return mailer + .sendMail({ + subject: subject, + templateName: templateName, + to: to, + templateData: { + appName: config.appName, + ...templateData, + }, + }) + .catch((error: Error) => { + log.error(error.stack); + + throw { + name: "SEND_EMAIL", + message: error.message, + statusCode: 500, + }; + }); +}; + +export default sendEmail; diff --git a/packages/multi-tenant/src/supertokens/utils/updateFields.ts b/packages/multi-tenant/src/supertokens/utils/updateFields.ts new file mode 100644 index 000000000..807ce76d9 --- /dev/null +++ b/packages/multi-tenant/src/supertokens/utils/updateFields.ts @@ -0,0 +1,30 @@ +import getMultiTenantConfig from "../../lib/getMultiTenantConfig"; + +import type { Tenant } from "../../types"; +import type { ApiConfig } from "@dzangolab/fastify-config"; + +interface FormField { + id: string; + value: string; +} + +const updateFields = ( + config: ApiConfig, + formFields: FormField[], + tenant: Tenant | undefined +) => { + if (tenant) { + formFields.find((field) => { + if (field.id === "email") { + field.value = + tenant[getMultiTenantConfig(config).table.columns.id] + + "_" + + field.value; + } + }); + } + + return formFields; +}; + +export default updateFields; diff --git a/packages/multi-tenant/src/tenantDiscoveryPlugin.ts b/packages/multi-tenant/src/tenantDiscoveryPlugin.ts index 7e11a8293..5ca71a4b7 100644 --- a/packages/multi-tenant/src/tenantDiscoveryPlugin.ts +++ b/packages/multi-tenant/src/tenantDiscoveryPlugin.ts @@ -2,8 +2,8 @@ import FastifyPlugin from "fastify-plugin"; import discoverTenant from "./lib/discoverTenant"; import getHost from "./lib/getHost"; +import getMultiTenantConfig from "./lib/getMultiTenantConfig"; -import type { Tenant } from "./types"; import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; const plugin = async ( @@ -23,7 +23,10 @@ const plugin = async ( const tenant = await discoverTenant(config, database, getHost(url)); if (tenant) { - request.tenant = tenant as Tenant; + request.tenant = tenant; + + request.dbSchema = + tenant[getMultiTenantConfig(config).table.columns.slug]; } } catch (error) { fastify.log.error(error); diff --git a/packages/multi-tenant/vite.config.ts b/packages/multi-tenant/vite.config.ts index cb2acd7ce..7e559ce30 100644 --- a/packages/multi-tenant/vite.config.ts +++ b/packages/multi-tenant/vite.config.ts @@ -22,6 +22,7 @@ export default defineConfig(({ mode }) => { ...Object.keys(dependencies), ...Object.keys(peerDependencies), "node:fs", + /supertokens-node+/, ], output: { exports: "named", @@ -32,11 +33,21 @@ export default defineConfig(({ mode }) => { "@dzangolab/postgres-migrations": "DzangolabPostgresMigrations", fastify: "Fastify", "fastify-plugin": "FastifyPlugin", + "@dzangolab/fastify-user": "DzangolabFastifyUser", humps: "Humps", mercurius: "Mercurius", + "lodash.merge": "LodashMerge", "node:fs": "NodeFs", pg: "Pg", slonik: "Slonik", + "supertokens-node": "SupertokensNode", + "supertokens-node/framework/fastify": "SupertokensFastify", + "supertokens-node/recipe/session/framework/fastify": + "SupertokensSessionFastify", + "supertokens-node/recipe/session": "SupertokensSession", + "supertokens-node/recipe/thirdpartyemailpassword": + "SupertokensThirdPartyEmailPassword", + "supertokens-node/recipe/userroles": "SupertokensUserRoles", zod: "Zod", }, }, diff --git a/packages/slonik/src/index.ts b/packages/slonik/src/index.ts index 1afaf9171..e04261b16 100644 --- a/packages/slonik/src/index.ts +++ b/packages/slonik/src/index.ts @@ -14,6 +14,7 @@ declare module "fastify" { } interface FastifyRequest { + dbSchema: string; slonik: { connect: (connectionRoutine: ConnectionRoutine) => Promise; pool: DatabasePool; diff --git a/packages/slonik/src/plugin.ts b/packages/slonik/src/plugin.ts index 000c6a446..14aafa5dd 100644 --- a/packages/slonik/src/plugin.ts +++ b/packages/slonik/src/plugin.ts @@ -24,6 +24,8 @@ const plugin = async ( fastify.log.info("Running database migrations"); await migrate(fastify.config); + fastify.decorateRequest("dbSchema", ""); + done(); }; diff --git a/packages/user/src/__test__/helpers/createConfig.ts b/packages/user/src/__test__/helpers/createConfig.ts index 1fb3b4f5d..126a02723 100644 --- a/packages/user/src/__test__/helpers/createConfig.ts +++ b/packages/user/src/__test__/helpers/createConfig.ts @@ -11,7 +11,7 @@ declare module "@dzangolab/fastify-config" { } const createConfig = (slonikConfig?: SlonikConfig) => { - const config: ApiConfig = { + const config = { appName: "app", appOrigin: ["http://localhost"], baseUrl: "http://localhost", @@ -36,7 +36,8 @@ const createConfig = (slonikConfig?: SlonikConfig) => { }, ...slonikConfig, }, - }; + user: {}, + } as ApiConfig; return config; }; diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index e09472b6a..a2b8ef7c0 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -35,4 +35,10 @@ export { default as userRoutes } from "./model/users/controller"; export { default as formatDate } from "./supertokens/utils/formatDate"; export type { ThirdPartyEmailPasswordRecipe } from "./supertokens/types"; -export type { AuthUser, UserCreateInput, UserUpdateInput, User } from "./types"; +export type { + AuthUser, + ChangePasswordInput, + UserCreateInput, + UserUpdateInput, + User, +} from "./types"; diff --git a/packages/user/src/model/users/controller.ts b/packages/user/src/model/users/controller.ts index 4324dbdcc..566fabacc 100644 --- a/packages/user/src/model/users/controller.ts +++ b/packages/user/src/model/users/controller.ts @@ -1,8 +1,6 @@ -import Service from "./service"; -import { ChangePasswordInput } from "../../types"; +import handlers from "./handlers"; -import type { FastifyInstance, FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyInstance } from "fastify"; const plugin = async ( fastify: FastifyInstance, @@ -18,25 +16,7 @@ const plugin = async ( { preHandler: fastify.verifySession(), }, - async (request: SessionRequest, reply: FastifyReply) => { - const service = new Service(request.config, request.slonik); - - const { limit, offset, filters, sort } = request.query as { - limit: number; - offset?: number; - filters?: string; - sort?: string; - }; - - const data = await service.list( - limit, - offset, - filters ? JSON.parse(filters) : undefined, - sort ? JSON.parse(sort) : undefined - ); - - reply.send(data); - } + handlers.users ); fastify.post( @@ -44,36 +24,7 @@ const plugin = async ( { preHandler: fastify.verifySession(), }, - async (request: SessionRequest, reply: FastifyReply) => { - try { - const session = request.session; - const requestBody = request.body as ChangePasswordInput; - const userId = session && session.getUserId(); - if (!userId) { - throw new Error("User not found in session"); - } - const oldPassword = requestBody.oldPassword ?? ""; - const newPassword = requestBody.newPassword ?? ""; - - const service = new Service(request.config, request.slonik); - const data = await service.changePassword( - userId, - oldPassword, - newPassword - ); - - reply.send(data); - } catch (error) { - fastify.log.error(error); - reply.status(500); - - reply.send({ - status: "ERROR", - message: "Oops! Something went wrong", - error, - }); - } - } + handlers.changePassword ); fastify.get( @@ -81,18 +32,7 @@ const plugin = async ( { preHandler: fastify.verifySession(), }, - async (request: SessionRequest, reply: FastifyReply) => { - const service = new Service(request.config, request.slonik); - const userId = request.session?.getUserId(); - - if (userId) { - reply.send(await service.findById(userId)); - } else { - fastify.log.error("Could not able to get user id from session"); - - throw new Error("Oops, Something went wrong"); - } - } + handlers.me ); done(); diff --git a/packages/user/src/model/users/handlers/changePassword.ts b/packages/user/src/model/users/handlers/changePassword.ts new file mode 100644 index 000000000..c70782238 --- /dev/null +++ b/packages/user/src/model/users/handlers/changePassword.ts @@ -0,0 +1,41 @@ +import Service from "../service"; + +import type { ChangePasswordInput } from "../../../types"; +import type { FastifyReply } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +const changePassword = async (request: SessionRequest, reply: FastifyReply) => { + try { + const session = request.session; + const requestBody = request.body as ChangePasswordInput; + const userId = session && session.getUserId(); + + if (!userId) { + throw new Error("User not found in session"); + } + + const oldPassword = requestBody.oldPassword ?? ""; + const newPassword = requestBody.newPassword ?? ""; + + const service = new Service( + request.config, + request.slonik, + request.dbSchema + ); + + const data = await service.changePassword(userId, oldPassword, newPassword); + + reply.send(data); + } catch (error) { + request.log.error(error); + reply.status(500); + + reply.send({ + status: "ERROR", + message: "Oops! Something went wrong", + error, + }); + } +}; + +export default changePassword; diff --git a/packages/user/src/model/users/handlers/index.ts b/packages/user/src/model/users/handlers/index.ts new file mode 100644 index 000000000..dad7ed72a --- /dev/null +++ b/packages/user/src/model/users/handlers/index.ts @@ -0,0 +1,5 @@ +import changePassword from "./changePassword"; +import me from "./me"; +import users from "./users"; + +export default { changePassword, me, users }; diff --git a/packages/user/src/model/users/handlers/me.ts b/packages/user/src/model/users/handlers/me.ts new file mode 100644 index 000000000..4835cf842 --- /dev/null +++ b/packages/user/src/model/users/handlers/me.ts @@ -0,0 +1,20 @@ +import Service from "../service"; + +import type { FastifyReply } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +const me = async (request: SessionRequest, reply: FastifyReply) => { + const service = new Service(request.config, request.slonik, request.dbSchema); + + const userId = request.session?.getUserId(); + + if (userId) { + reply.send(await service.findById(userId)); + } else { + request.log.error("Could not able to get user id from session"); + + throw new Error("Oops, Something went wrong"); + } +}; + +export default me; diff --git a/packages/user/src/model/users/handlers/users.ts b/packages/user/src/model/users/handlers/users.ts new file mode 100644 index 000000000..4cc3c8fa8 --- /dev/null +++ b/packages/user/src/model/users/handlers/users.ts @@ -0,0 +1,26 @@ +import Service from "../service"; + +import type { FastifyReply } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +const users = async (request: SessionRequest, reply: FastifyReply) => { + const service = new Service(request.config, request.slonik, request.dbSchema); + + const { limit, offset, filters, sort } = request.query as { + limit: number; + offset?: number; + filters?: string; + sort?: string; + }; + + const data = await service.list( + limit, + offset, + filters ? JSON.parse(filters) : undefined, + sort ? JSON.parse(sort) : undefined + ); + + reply.send(data); +}; + +export default users; diff --git a/packages/user/src/model/users/resolver.ts b/packages/user/src/model/users/resolver.ts index 753208cfe..34669ae4c 100644 --- a/packages/user/src/model/users/resolver.ts +++ b/packages/user/src/model/users/resolver.ts @@ -14,7 +14,11 @@ const Mutation = { }, context: MercuriusContext ) => { - const service = new Service(context.config, context.database); + const service = new Service( + context.config, + context.database, + context.dbSchema + ); try { if (context.user?.id) { @@ -48,12 +52,17 @@ const Mutation = { const Query = { me: async ( parent: unknown, - arguments_: unknown, + arguments_: Record, context: MercuriusContext ) => { - const service = new Service(context.config, context.database); + const service = new Service( + context.config, + context.database, + context.dbSchema + ); + if (context.user?.id) { - return service.findById(context.user.id); + return await service.findById(context.user.id); } else { context.app.log.error( "Could not able to get user id from mercurius context" @@ -73,7 +82,11 @@ const Query = { arguments_: { id: string }, context: MercuriusContext ) => { - const service = new Service(context.config, context.database); + const service = new Service( + context.config, + context.database, + context.dbSchema + ); return await service.findById(arguments_.id); }, @@ -88,7 +101,11 @@ const Query = { }, context: MercuriusContext ) => { - const service = new Service(context.config, context.database); + const service = new Service( + context.config, + context.database, + context.dbSchema + ); return await service.list( arguments_.limit, diff --git a/packages/user/src/types/index.ts b/packages/user/src/types/index.ts index 7c4dfd3b2..a40471d7f 100644 --- a/packages/user/src/types/index.ts +++ b/packages/user/src/types/index.ts @@ -1,3 +1,6 @@ +import type { PaginatedList } from "@dzangolab/fastify-slonik"; +import type { MercuriusContext } from "mercurius"; +import type { QueryResultRow } from "slonik"; import type { User as SupertokensUser } from "supertokens-node/recipe/thirdpartyemailpassword"; interface ChangePasswordInput { @@ -15,6 +18,16 @@ interface PasswordErrorMessages { weak?: string; } +interface Resolver { + [key: string]: ( + parent: unknown, + argyments_: { + [key: string]: unknown; + }, + context: MercuriusContext + ) => Promise>; +} + interface User { id: string; email: string; @@ -34,6 +47,7 @@ export type { ChangePasswordInput, EmailErrorMessages, PasswordErrorMessages, + Resolver, User, UserCreateInput, UserUpdateInput, diff --git a/packages/user/src/userContext.ts b/packages/user/src/userContext.ts index 72b17ef86..f9a0c6984 100644 --- a/packages/user/src/userContext.ts +++ b/packages/user/src/userContext.ts @@ -14,7 +14,7 @@ const userContext = async ( request: FastifyRequest, reply: FastifyReply ) => { - const { config, slonik } = request; + const { config, slonik, dbSchema } = request; const session = await Session.getSession(request, wrapResponse(reply), { sessionRequired: false, @@ -22,12 +22,12 @@ const userContext = async ( const userId = session?.getUserId(); - if (userId) { + if (userId && !context.user) { const service: UserService< User & QueryResultRow, UserCreateInput, UserUpdateInput - > = new UserService(config, slonik); + > = new UserService(config, slonik, dbSchema); /* eslint-disable-next-line unicorn/no-null */ let user: User | null = null; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 907577e2d..cca2a6014 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -212,6 +212,9 @@ importers: humps: specifier: 2.0.1 version: 2.0.1 + lodash.merge: + specifier: 4.6.2 + version: 4.6.2 pg: specifier: 8.8.0 version: 8.8.0 @@ -219,15 +222,24 @@ importers: '@dzangolab/fastify-config': specifier: 0.31.3 version: link:../config + '@dzangolab/fastify-mailer': + specifier: 0.31.3 + version: link:../mailer '@dzangolab/fastify-mercurius': specifier: 0.31.3 version: link:../mercurius '@dzangolab/fastify-slonik': specifier: 0.31.3 version: link:../slonik + '@dzangolab/fastify-user': + specifier: 0.31.3 + version: link:../user '@types/humps': specifier: 2.0.2 version: 2.0.2 + '@types/lodash.merge': + specifier: 4.6.7 + version: 4.6.7 '@types/node': specifier: 18.15.11 version: 18.15.11 @@ -264,6 +276,9 @@ importers: slonik: specifier: 33.1.4 version: 33.1.4(zod@3.21.4) + supertokens-node: + specifier: 12.1.6 + version: 12.1.6 tsconfig: specifier: 0.31.3 version: link:../../tools/tsconfig @@ -1496,6 +1511,16 @@ packages: '@types/node': 18.15.11 dev: true + /@types/lodash.merge@4.6.7: + resolution: {integrity: sha512-OwxUJ9E50gw3LnAefSHJPHaBLGEKmQBQ7CZe/xflHkyy/wH2zVyEIAKReHvVrrn7zKdF58p16We9kMfh7v0RRQ==} + dependencies: + '@types/lodash': 4.14.194 + dev: true + + /@types/lodash@4.14.194: + resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==} + dev: true + /@types/mime@3.0.1: resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} dev: true @@ -2376,7 +2401,7 @@ packages: js-string-escape: 1.0.1 lodash: 4.17.21 md5-hex: 3.0.1 - semver: 7.5.0 + semver: 7.3.8 well-known-symbols: 2.0.0 dev: true @@ -4426,7 +4451,7 @@ packages: jws: 3.2.2 lodash: 4.17.21 ms: 2.1.3 - semver: 7.5.0 + semver: 7.3.8 dev: true /juice@9.0.0: @@ -5320,7 +5345,7 @@ packages: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.11.0 - semver: 7.5.0 + semver: 7.3.8 validate-npm-package-license: 3.0.4 dev: true