diff --git a/packages/multi-tenant/src/lib/createTenantOwnerRole.ts b/packages/multi-tenant/src/lib/createTenantOwnerRole.ts index 4a3a27fea..c0242593e 100644 --- a/packages/multi-tenant/src/lib/createTenantOwnerRole.ts +++ b/packages/multi-tenant/src/lib/createTenantOwnerRole.ts @@ -1,9 +1,34 @@ -import UserRoles from "supertokens-node/recipe/userroles"; +import { RoleService } from "@dzangolab/fastify-user"; import { ROLE_TENANT_OWNER } from "../constants"; -const createTenantOwnerRole = async () => { - await UserRoles.createNewRoleOrAddPermissions(ROLE_TENANT_OWNER, []); +import type { ApiConfig } from "@dzangolab/fastify-config"; +import type { Database } from "@dzangolab/fastify-slonik"; +import type { + Role, + RoleCreateInput, + RoleUpdateInput, +} from "@dzangolab/fastify-user"; + +const createTenantOwnerRole = async (config: ApiConfig, slonik: Database) => { + const service = new RoleService( + config, + slonik + ); + + const filteredCount = await service.count({ + key: "role", + operator: "eq", + value: ROLE_TENANT_OWNER, + }); + + if (!filteredCount) { + await service.create({ + role: ROLE_TENANT_OWNER, + permissions: [], + default: false, + }); + } }; export default createTenantOwnerRole; diff --git a/packages/multi-tenant/src/lib/updateContext.ts b/packages/multi-tenant/src/lib/updateContext.ts index c150c66fc..d1fc96655 100644 --- a/packages/multi-tenant/src/lib/updateContext.ts +++ b/packages/multi-tenant/src/lib/updateContext.ts @@ -1,9 +1,9 @@ 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 { User } from "@dzangolab/fastify-user"; import type { FastifyRequest, FastifyReply } from "fastify"; import type { MercuriusContext } from "mercurius"; @@ -26,7 +26,7 @@ const updateContext = async ( const service = getUserService(config, slonik, tenant); /* eslint-disable-next-line unicorn/no-null */ - let user; + let user: User | null = null; try { user = await service.findById(userId); @@ -39,10 +39,8 @@ const updateContext = async ( throw new Error("Unable to find user"); } - const { roles } = await UserRoles.getRolesForUser(userId); - context.user = user; - context.roles = roles; + context.roles = user.roles; } }; diff --git a/packages/multi-tenant/src/model/tenants/handlers/all.ts b/packages/multi-tenant/src/model/tenants/handlers/all.ts index 0a6ded340..9df151a03 100644 --- a/packages/multi-tenant/src/model/tenants/handlers/all.ts +++ b/packages/multi-tenant/src/model/tenants/handlers/all.ts @@ -1,8 +1,8 @@ -import UserRoles from "supertokens-node/recipe/userroles"; - import { ROLE_TENANT_OWNER } from "../../../constants"; +import getUserService from "../../../lib/getUserService"; import Service from "../service"; +import type { User } from "@dzangolab/fastify-user"; import type { FastifyReply } from "fastify"; import type { SessionRequest } from "supertokens-node/framework/fastify"; @@ -24,11 +24,13 @@ const all = async (request: SessionRequest, reply: FastifyReply) => { const service = new Service(request.config, request.slonik, request.dbSchema); - const { roles } = await UserRoles.getRolesForUser(userId); + const userService = getUserService(request.config, request.slonik); + + const user = (await userService.findById(userId)) as User; // [DU 2024-JAN-15] TODO: address the scenario in which a user possesses // both roles: ADMIN and TENANT_OWNER - if (roles.includes(ROLE_TENANT_OWNER)) { + if (user.roles.some(({ role }) => role === ROLE_TENANT_OWNER)) { service.ownerId = userId; } diff --git a/packages/multi-tenant/src/model/tenants/handlers/tenant.ts b/packages/multi-tenant/src/model/tenants/handlers/tenant.ts index 59fadaa64..b00366856 100644 --- a/packages/multi-tenant/src/model/tenants/handlers/tenant.ts +++ b/packages/multi-tenant/src/model/tenants/handlers/tenant.ts @@ -1,8 +1,8 @@ -import UserRoles from "supertokens-node/recipe/userroles"; - import { ROLE_TENANT_OWNER } from "../../../constants"; +import getUserService from "../../../lib/getUserService"; import Service from "../service"; +import type { User } from "@dzangolab/fastify-user"; import type { FastifyReply } from "fastify"; import type { SessionRequest } from "supertokens-node/framework/fastify"; @@ -24,11 +24,13 @@ const tenant = async (request: SessionRequest, reply: FastifyReply) => { const service = new Service(request.config, request.slonik, request.dbSchema); - const { roles } = await UserRoles.getRolesForUser(userId); + const userService = getUserService(request.config, request.slonik); + + const user = (await userService.findById(userId)) as User; // [DU 2024-JAN-15] TODO: address the scenario in which a user possesses // both roles: ADMIN and TENANT_OWNER - if (roles.includes(ROLE_TENANT_OWNER)) { + if (user.roles.some(({ role }) => role === ROLE_TENANT_OWNER)) { service.ownerId = userId; } diff --git a/packages/multi-tenant/src/model/tenants/handlers/tenants.ts b/packages/multi-tenant/src/model/tenants/handlers/tenants.ts index 868a15738..de1cf1b7b 100644 --- a/packages/multi-tenant/src/model/tenants/handlers/tenants.ts +++ b/packages/multi-tenant/src/model/tenants/handlers/tenants.ts @@ -1,8 +1,8 @@ -import UserRoles from "supertokens-node/recipe/userroles"; - import { ROLE_TENANT_OWNER } from "../../../constants"; +import getUserService from "../../../lib/getUserService"; import Service from "../service"; +import type { User } from "@dzangolab/fastify-user"; import type { FastifyReply } from "fastify"; import type { SessionRequest } from "supertokens-node/framework/fastify"; @@ -24,11 +24,13 @@ const tenants = async (request: SessionRequest, reply: FastifyReply) => { const service = new Service(request.config, request.slonik, request.dbSchema); - const { roles } = await UserRoles.getRolesForUser(userId); + const userService = getUserService(request.config, request.slonik); + + const user = (await userService.findById(userId)) as User; // [DU 2024-JAN-15] TODO: address the scenario in which a user possesses // both roles: ADMIN and TENANT_OWNER - if (roles.includes(ROLE_TENANT_OWNER)) { + if (user.roles.some(({ role }) => role === ROLE_TENANT_OWNER)) { service.ownerId = userId; } diff --git a/packages/multi-tenant/src/model/tenants/resolver.ts b/packages/multi-tenant/src/model/tenants/resolver.ts index d4cf7430b..9468cb76a 100644 --- a/packages/multi-tenant/src/model/tenants/resolver.ts +++ b/packages/multi-tenant/src/model/tenants/resolver.ts @@ -1,9 +1,10 @@ +import { User } from "@dzangolab/fastify-user"; import mercurius from "mercurius"; -import UserRoles from "supertokens-node/recipe/userroles"; import Service from "./service"; import { ROLE_TENANT_OWNER } from "../../constants"; import getMultiTenantConfig from "../../lib/getMultiTenantConfig"; +import getUserService from "../../lib/getUserService"; import type { TenantCreateInput } from "./../../types"; import type { FilterInput, SortInput } from "@dzangolab/fastify-slonik"; @@ -99,11 +100,17 @@ const Query = { context.dbSchema ); - const { roles } = await UserRoles.getRolesForUser(userId); + const userService = getUserService( + context.config, + context.database, + context.tenant + ); + + const { roles } = (await userService.findById(userId)) as User; // [DU 2024-JAN-15] TODO: address the scenario in which a user possesses // both roles: ADMIN and TENANT_OWNER - if (roles.includes(ROLE_TENANT_OWNER)) { + if (roles.some(({ role }) => role === ROLE_TENANT_OWNER)) { service.ownerId = userId; } @@ -138,11 +145,17 @@ const Query = { context.dbSchema ); - const { roles } = await UserRoles.getRolesForUser(userId); + const userService = getUserService( + context.config, + context.database, + context.tenant + ); + + const { roles } = (await userService.findById(userId)) as User; // [DU 2024-JAN-15] TODO: address the scenario in which a user possesses // both roles: ADMIN and TENANT_OWNER - if (roles.includes(ROLE_TENANT_OWNER)) { + if (roles.some(({ role }) => role === ROLE_TENANT_OWNER)) { service.ownerId = userId; } @@ -182,11 +195,17 @@ const Query = { context.dbSchema ); - const { roles } = await UserRoles.getRolesForUser(userId); + const userService = getUserService( + context.config, + context.database, + context.tenant + ); + + const { roles } = (await userService.findById(userId)) as User; // [DU 2024-JAN-15] TODO: address the scenario in which a user possesses // both roles: ADMIN and TENANT_OWNER - if (roles.includes(ROLE_TENANT_OWNER)) { + if (roles.some(({ role }) => role === ROLE_TENANT_OWNER)) { service.ownerId = userId; } diff --git a/packages/multi-tenant/src/plugin.ts b/packages/multi-tenant/src/plugin.ts index 2678c5b16..bde369a5b 100644 --- a/packages/multi-tenant/src/plugin.ts +++ b/packages/multi-tenant/src/plugin.ts @@ -19,7 +19,7 @@ const plugin = async ( // Register domain discovery plugin await fastify.register(tenantDiscoveryPlugin); - const { config } = fastify; + const { config, slonik } = fastify; const supertokensConfig = { recipes }; @@ -27,7 +27,7 @@ const plugin = async ( config.user.supertokens = merge(supertokensConfig, config.user.supertokens); fastify.addHook("onReady", async () => { - await createTenantOwnerRole(); + await createTenantOwnerRole(config, slonik); }); done(); 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 index 9d3c11e0f..72ba529fa 100644 --- 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 @@ -1,7 +1,11 @@ -import { areRolesExist, sendEmail, verifyEmail } from "@dzangolab/fastify-user"; +import { + areRolesExist, + getRolesByNames, + sendEmail, + verifyEmail, +} from "@dzangolab/fastify-user"; import { deleteUser } from "supertokens-node"; import EmailVerification from "supertokens-node/recipe/emailverification"; -import UserRoles from "supertokens-node/recipe/userroles"; import getUserService from "../../../lib/getUserService"; import Email from "../../utils/email"; @@ -19,7 +23,7 @@ const emailPasswordSignUp = ( return async (input) => { const roles = (input.userContext.roles || []) as string[]; - if (!(await areRolesExist(roles))) { + if (!(await areRolesExist(roles, config, slonik))) { log.error(`At least one role from ${roles.join(", ")} does not exist.`); throw { @@ -73,22 +77,19 @@ const emailPasswordSignUp = ( }; } + const rolesResponse = await getRolesByNames(roles, config, slonik); + + const rolesIds = rolesResponse.map(({ id }) => id); + + await userService.addRolesToUser(originalResponse.user.id, rolesIds); + + user = (await userService.findById(originalResponse.user.id)) as User; + originalResponse.user = { ...originalResponse.user, ...user, }; - for (const role of roles) { - const rolesResponse = await UserRoles.addRoleToUser( - originalResponse.user.id, - role - ); - - if (rolesResponse.status !== "OK") { - log.error(rolesResponse.status); - } - } - if (config.user.features?.signUp?.emailVerification) { try { if (input.userContext.autoVerifyEmail) { 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 index c4a103062..7a770c39a 100644 --- 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 @@ -1,7 +1,6 @@ import { areRolesExist } from "@dzangolab/fastify-user"; import { deleteUser } from "supertokens-node"; import { getUserByThirdPartyInfo } from "supertokens-node/recipe/thirdpartyemailpassword"; -import UserRoles from "supertokens-node/recipe/userroles"; import getMultiTenantConfig from "../../../lib/getMultiTenantConfig"; @@ -13,7 +12,7 @@ const thirdPartySignInUp = ( originalImplementation: RecipeInterface, fastify: FastifyInstance ): RecipeInterface["thirdPartySignInUp"] => { - const { config, log } = fastify; + const { config, log, slonik } = fastify; return async (input) => { const roles = (input.userContext.roles || []) as string[]; @@ -43,29 +42,20 @@ const thirdPartySignInUp = ( input ); - if (originalResponse.status === "OK" && originalResponse.createdNewUser) { - if (!(await areRolesExist(roles))) { - await deleteUser(originalResponse.user.id); + if ( + originalResponse.status === "OK" && + originalResponse.createdNewUser && + !(await areRolesExist(roles, config, slonik)) + ) { + await deleteUser(originalResponse.user.id); - log.error(`At least one role from ${roles.join(", ")} does not exist.`); + log.error(`At least one role from ${roles.join(", ")} does not exist.`); - throw { - name: "SIGN_UP_FAILED", - message: "Something went wrong", - statusCode: 500, - } as FastifyError; - } - - for (const role of roles) { - const rolesResponse = await UserRoles.addRoleToUser( - originalResponse.user.id, - role - ); - - if (rolesResponse.status !== "OK") { - log.error(rolesResponse.status); - } - } + throw { + name: "SIGN_UP_FAILED", + message: "Something went wrong", + statusCode: 500, + } as FastifyError; } return originalResponse; 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 index 4d3759dc1..9f06a2637 100644 --- 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 @@ -1,5 +1,5 @@ import { formatDate } from "@dzangolab/fastify-slonik"; -import { ROLE_USER } from "@dzangolab/fastify-user"; +import { ROLE_USER, getRolesByNames } from "@dzangolab/fastify-user"; import { deleteUser } from "supertokens-node"; import { ROLE_TENANT_OWNER } from "../../../constants"; @@ -80,7 +80,18 @@ const thirdPartySignInUpPOST = ( throw new Error("User not found"); } - user.roles = input.userContext.roles; + const rolesResponse = await getRolesByNames( + input.userContext.roles, + config, + slonik + ); + + const rolesIds = rolesResponse.map(({ id }) => id); + + await userService.addRolesToUser(originalResponse.user.id, rolesIds); + + user = (await userService.findById(originalResponse.user.id)) as User; + /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ } catch (error: any) { log.error("Error while creating user"); @@ -123,14 +134,11 @@ const thirdPartySignInUpPOST = ( }); } return { - status: "OK", - createdNewUser: originalResponse.createdNewUser, + ...originalResponse, user: { ...originalResponse.user, ...user, }, - session: originalResponse.session, - authCodeResponse: originalResponse.authCodeResponse, }; } diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index 428be7e8c..69b47b48f 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -7,7 +7,12 @@ import userHandlers from "./model/users/handlers"; import UserService from "./model/users/service"; import type { SupertokensConfig } from "./supertokens"; -import type { IsEmailOptions, StrongPasswordOptions, User } from "./types"; +import type { + IsEmailOptions, + Role, + StrongPasswordOptions, + User, +} from "./types"; import type { Invitation } from "./types/invitation"; import type { FastifyRequest } from "fastify"; @@ -19,7 +24,7 @@ declare module "fastify" { declare module "mercurius" { interface MercuriusContext { - roles: string[] | undefined; + roles: Omit[] | undefined; user: User | undefined; } } @@ -118,6 +123,7 @@ export { default as validateEmail } from "./validator/email"; export { default as validatePassword } from "./validator/password"; export { default as hasUserPermission } from "./lib/hasUserPermission"; export { default as CustomApiError } from "./customApiError"; +export { default as getRolesByNames } from "./lib/getRolesByNames"; export { emailSchema, passwordSchema, roleSchema } from "./schemas"; export * from "./constants"; @@ -131,9 +137,9 @@ export type { Role, RoleCreateInput, RoleUpdateInput, + User, UserCreateInput, UserUpdateInput, - User, } from "./types"; export type { Invitation, diff --git a/packages/user/src/lib/getRolesByNames.ts b/packages/user/src/lib/getRolesByNames.ts new file mode 100644 index 000000000..3562b088a --- /dev/null +++ b/packages/user/src/lib/getRolesByNames.ts @@ -0,0 +1,32 @@ +import RoleService from "../model/roles/service"; + +import type { Role, RoleCreateInput, RoleUpdateInput } from "../types"; +import type { ApiConfig } from "@dzangolab/fastify-config"; +import type { Database } from "@dzangolab/fastify-slonik"; + +const getRolesByNames = async ( + roles: string[], + config: ApiConfig, + slonik: Database, + schema?: string +): Promise => { + const service = new RoleService( + config, + slonik, + schema + ); + + const filteredRoles = await service.all( + ["id", "role", "default", "permissions"], + undefined, + { + key: "role", + operator: "in", + value: roles.join(","), + } + ); + + return filteredRoles as Role[]; +}; + +export default getRolesByNames; diff --git a/packages/user/src/lib/hasUserPermission.ts b/packages/user/src/lib/hasUserPermission.ts index 032696e15..2c4820fae 100644 --- a/packages/user/src/lib/hasUserPermission.ts +++ b/packages/user/src/lib/hasUserPermission.ts @@ -1,27 +1,25 @@ -import UserRoles from "supertokens-node/recipe/userroles"; - +import getRolesByNames from "./getRolesByNames"; +import getUserService from "./getUserService"; import { ROLE_SUPERADMIN } from "../constants"; +import type { Role, User } from "../types"; import type { FastifyInstance } from "fastify"; -const getPermissions = async (roles: string[]) => { - let permissions: string[] = []; - - for (const role of roles) { - const response = await UserRoles.getPermissionsForRole(role); +const getPermissions = async (fastify: FastifyInstance, roles: Role[]) => { + const rolesWithPermissions = (await getRolesByNames( + roles.map(({ role }) => role), + fastify.config, + fastify.slonik + )) as Required[]; - if (response.status === "OK") { - permissions = [...new Set([...permissions, ...response.permissions])]; - } - } - - return permissions; + return rolesWithPermissions.flatMap((role) => role.permissions); }; const hasUserPermission = async ( fastify: FastifyInstance, userId: string, - permission: string + permission: string, + dbSchema?: string ): Promise => { const permissions = fastify.config.user.permissions; @@ -30,14 +28,16 @@ const hasUserPermission = async ( return true; } - const { roles } = await UserRoles.getRolesForUser(userId); + const userService = getUserService(fastify.config, fastify.slonik, dbSchema); + + const { roles } = (await userService.findById(userId)) as User; // Allow if user has super admin role - if (roles && roles.includes(ROLE_SUPERADMIN)) { + if (roles && roles.some(({ role }) => role === ROLE_SUPERADMIN)) { return true; } - const rolePermissions = await getPermissions(roles); + const rolePermissions = await getPermissions(fastify, roles); if (!rolePermissions || !rolePermissions.includes(permission)) { return false; diff --git a/packages/user/src/mercurius-auth/hasPermissionPlugin.ts b/packages/user/src/mercurius-auth/hasPermissionPlugin.ts index dfa10b07a..faf837f7c 100644 --- a/packages/user/src/mercurius-auth/hasPermissionPlugin.ts +++ b/packages/user/src/mercurius-auth/hasPermissionPlugin.ts @@ -21,7 +21,8 @@ const plugin = FastifyPlugin(async (fastify: FastifyInstance) => { const hasPermission = await hasUserPermission( context.app, context.user?.id, - permission + permission, + context.dbSchema ); if (!hasPermission) { diff --git a/packages/user/src/middlewares/hasPermission.ts b/packages/user/src/middlewares/hasPermission.ts index 3c89afdb1..a2fb031df 100644 --- a/packages/user/src/middlewares/hasPermission.ts +++ b/packages/user/src/middlewares/hasPermission.ts @@ -1,5 +1,4 @@ import { Error as STError } from "supertokens-node/recipe/session"; -import UserRoles from "supertokens-node/recipe/userroles"; import hasUserPermission from "../lib/hasUserPermission"; @@ -17,14 +16,21 @@ const hasPermission = }); } - if (!(await hasUserPermission(request.server, userId, permission))) { + if ( + !(await hasUserPermission( + request.server, + userId, + permission, + request.dbSchema + )) + ) { // this error tells SuperTokens to return a 403 http response. throw new STError({ type: "INVALID_CLAIMS", message: "Not have enough permission", payload: [ { - id: UserRoles.PermissionClaim.key, + id: "st-prem", reason: { message: "Not have enough permission", expectedToInclude: permission, diff --git a/packages/user/src/model/users/handlers/adminSignUp.ts b/packages/user/src/model/users/handlers/adminSignUp.ts index 7fbe1f84a..6aeab06cb 100644 --- a/packages/user/src/model/users/handlers/adminSignUp.ts +++ b/packages/user/src/model/users/handlers/adminSignUp.ts @@ -1,10 +1,11 @@ import { createNewSession } from "supertokens-node/recipe/session"; import { emailPasswordSignUp } from "supertokens-node/recipe/thirdpartyemailpassword"; -import UserRoles from "supertokens-node/recipe/userroles"; import { ROLE_ADMIN, ROLE_SUPERADMIN } from "../../../constants"; +import getUserService from "../../../lib/getUserService"; import validateEmail from "../../../validator/email"; import validatePassword from "../../../validator/password"; +import RoleService from "../../roles/service"; import type { FastifyReply, FastifyRequest } from "fastify"; @@ -14,36 +15,12 @@ interface FieldInput { } const adminSignUp = async (request: FastifyRequest, reply: FastifyReply) => { - const { body, config, log } = request as FastifyRequest<{ + const { body, config, log, slonik } = request as FastifyRequest<{ Body: FieldInput; }>; try { const { email, password } = body; - // check if already admin user exists - const adminUsers = await UserRoles.getUsersThatHaveRole(ROLE_ADMIN); - const superAdminUsers = await UserRoles.getUsersThatHaveRole( - ROLE_SUPERADMIN - ); - - if ( - adminUsers.status === "UNKNOWN_ROLE_ERROR" && - superAdminUsers.status === "UNKNOWN_ROLE_ERROR" - ) { - return reply.send({ - status: "ERROR", - message: adminUsers.status, - }); - } else if ( - (adminUsers.status === "OK" && adminUsers.users.length > 0) || - (superAdminUsers.status === "OK" && superAdminUsers.users.length > 0) - ) { - return reply.send({ - status: "ERROR", - message: "First admin user already exists", - }); - } - // check if the email is valid const emailResult = validateEmail(email, config); @@ -64,12 +41,31 @@ const adminSignUp = async (request: FastifyRequest, reply: FastifyReply) => { }); } + const userService = getUserService(config, slonik); + + const isAdminExists = await userService.isAdminExists(); + + if (isAdminExists) { + return reply.send({ + status: "ERROR", + message: "First admin user already exists", + }); + } + + const roleService = new RoleService(config, slonik); + + const superadminFilteredCount = await roleService.count({ + key: "role", + operator: "eq", + value: ROLE_SUPERADMIN, + }); + // signup const signUpResponse = await emailPasswordSignUp(email, password, { autoVerifyEmail: true, roles: [ ROLE_ADMIN, - ...(superAdminUsers.status === "OK" ? [ROLE_SUPERADMIN] : []), + ...(superadminFilteredCount ? [ROLE_SUPERADMIN] : []), ], _default: { request: { diff --git a/packages/user/src/model/users/handlers/canAdminSignUp.ts b/packages/user/src/model/users/handlers/canAdminSignUp.ts index c2683a743..aa1ccab0b 100644 --- a/packages/user/src/model/users/handlers/canAdminSignUp.ts +++ b/packages/user/src/model/users/handlers/canAdminSignUp.ts @@ -1,31 +1,16 @@ -import UserRoles from "supertokens-node/recipe/userroles"; - -import { ROLE_ADMIN, ROLE_SUPERADMIN } from "../../../constants"; +import getUserService from "../../../lib/getUserService"; import type { FastifyReply, FastifyRequest } from "fastify"; const canAdminSignUp = async (request: FastifyRequest, reply: FastifyReply) => { - const { log } = request; + const { config, log, slonik } = request; try { - // check if already admin user exists - const adminUsers = await UserRoles.getUsersThatHaveRole(ROLE_ADMIN); - const superAdminUsers = await UserRoles.getUsersThatHaveRole( - ROLE_SUPERADMIN - ); - - if ( - adminUsers.status === "UNKNOWN_ROLE_ERROR" && - superAdminUsers.status === "UNKNOWN_ROLE_ERROR" - ) { - return reply.send({ - status: "ERROR", - message: adminUsers.status, - }); - } else if ( - (adminUsers.status === "OK" && adminUsers.users.length > 0) || - (superAdminUsers.status === "OK" && superAdminUsers.users.length > 0) - ) { + const userService = getUserService(config, slonik); + + const isAdminExists = await userService.isAdminExists(); + + if (isAdminExists) { return reply.send({ signUp: false }); } diff --git a/packages/user/src/model/users/resolver.ts b/packages/user/src/model/users/resolver.ts index b6cb17041..8df2c7156 100644 --- a/packages/user/src/model/users/resolver.ts +++ b/packages/user/src/model/users/resolver.ts @@ -1,13 +1,13 @@ import mercurius from "mercurius"; import { createNewSession } from "supertokens-node/recipe/session"; import { emailPasswordSignUp } from "supertokens-node/recipe/thirdpartyemailpassword"; -import UserRoles from "supertokens-node/recipe/userroles"; import filterUserUpdateInput from "./filterUserUpdateInput"; import { ROLE_ADMIN, ROLE_SUPERADMIN } from "../../constants"; import getUserService from "../../lib/getUserService"; import validateEmail from "../../validator/email"; import validatePassword from "../../validator/password"; +import RoleService from "../roles/service"; import type { UserUpdateInput } from "./../../types"; import type { FilterInput, SortInput } from "@dzangolab/fastify-slonik"; @@ -24,37 +24,11 @@ const Mutation = { }, context: MercuriusContext ) => { - const { app, config, reply } = context; + const { app, config, database, reply } = context; try { const { email, password } = arguments_.data; - // check if already admin user exists - const adminUsers = await UserRoles.getUsersThatHaveRole(ROLE_ADMIN); - const superAdminUsers = await UserRoles.getUsersThatHaveRole( - ROLE_SUPERADMIN - ); - - let errorMessage: string | undefined; - - if ( - adminUsers.status === "UNKNOWN_ROLE_ERROR" && - superAdminUsers.status === "UNKNOWN_ROLE_ERROR" - ) { - errorMessage = adminUsers.status; - } else if ( - (adminUsers.status === "OK" && adminUsers.users.length > 0) || - (superAdminUsers.status === "OK" && superAdminUsers.users.length > 0) - ) { - errorMessage = "First admin user already exists"; - } - - if (errorMessage) { - const mercuriusError = new mercurius.ErrorWithProps(errorMessage); - - return mercuriusError; - } - // check if the email is valid const emailResult = validateEmail(email, config); @@ -77,12 +51,32 @@ const Mutation = { return mercuriusError; } + const userService = getUserService(config, database); + + const isAdminExists = await userService.isAdminExists(); + + if (isAdminExists) { + const mercuriusError = new mercurius.ErrorWithProps( + "First admin user already exists" + ); + + return mercuriusError; + } + + const roleService = new RoleService(config, database); + + const superadminFilteredCount = await roleService.count({ + key: "role", + operator: "eq", + value: ROLE_SUPERADMIN, + }); + // signup const signUpResponse = await emailPasswordSignUp(email, password, { autoVerifyEmail: true, roles: [ ROLE_ADMIN, - ...(superAdminUsers.status === "OK" ? [ROLE_SUPERADMIN] : []), + ...(superadminFilteredCount ? [ROLE_SUPERADMIN] : []), ], _default: { request: { @@ -255,26 +249,14 @@ const Query = { arguments_: { id: string }, context: MercuriusContext ) => { - const { app } = context; + const { app, database, config } = context; try { - // check if already admin user exists - const adminUsers = await UserRoles.getUsersThatHaveRole(ROLE_ADMIN); - const superAdminUsers = await UserRoles.getUsersThatHaveRole( - ROLE_SUPERADMIN - ); + const userService = getUserService(config, database); - if ( - adminUsers.status === "UNKNOWN_ROLE_ERROR" && - superAdminUsers.status === "UNKNOWN_ROLE_ERROR" - ) { - const mercuriusError = new mercurius.ErrorWithProps(adminUsers.status); + const isAdminExists = await userService.isAdminExists(); - return mercuriusError; - } else if ( - (adminUsers.status === "OK" && adminUsers.users.length > 0) || - (superAdminUsers.status === "OK" && superAdminUsers.users.length > 0) - ) { + if (isAdminExists) { return { signUp: false }; } diff --git a/packages/user/src/supertokens/recipes/config/third-party-email-password/emailPasswordSignUp.ts b/packages/user/src/supertokens/recipes/config/third-party-email-password/emailPasswordSignUp.ts index dac59697f..41c5ad38f 100644 --- a/packages/user/src/supertokens/recipes/config/third-party-email-password/emailPasswordSignUp.ts +++ b/packages/user/src/supertokens/recipes/config/third-party-email-password/emailPasswordSignUp.ts @@ -1,7 +1,7 @@ import { deleteUser } from "supertokens-node"; import EmailVerification from "supertokens-node/recipe/emailverification"; -import UserRoles from "supertokens-node/recipe/userroles"; +import getRolesByNames from "../../../../lib/getRolesByNames"; import getUserService from "../../../../lib/getUserService"; import sendEmail from "../../../../lib/sendEmail"; import verifyEmail from "../../../../lib/verifyEmail"; @@ -20,7 +20,7 @@ const emailPasswordSignUp = ( return async (input) => { const roles = (input.userContext.roles || []) as string[]; - if (!(await areRolesExist(roles))) { + if (!(await areRolesExist(roles, config, slonik))) { log.error(`At least one role from ${roles.join(", ")} does not exist.`); throw { @@ -62,22 +62,19 @@ const emailPasswordSignUp = ( }; } + const rolesResponse = await getRolesByNames(roles, config, slonik); + + const rolesIds = rolesResponse.map(({ id }) => id); + + await userService.addRolesToUser(originalResponse.user.id, rolesIds); + + user = (await userService.findById(originalResponse.user.id)) as User; + originalResponse.user = { ...originalResponse.user, ...user, }; - for (const role of roles) { - const rolesResponse = await UserRoles.addRoleToUser( - originalResponse.user.id, - role - ); - - if (rolesResponse.status !== "OK") { - log.error(rolesResponse.status); - } - } - if (config.user.features?.signUp?.emailVerification) { try { if (input.userContext.autoVerifyEmail) { diff --git a/packages/user/src/supertokens/recipes/config/third-party-email-password/thirdPartySignInUp.ts b/packages/user/src/supertokens/recipes/config/third-party-email-password/thirdPartySignInUp.ts index 6b3463674..6e1442173 100644 --- a/packages/user/src/supertokens/recipes/config/third-party-email-password/thirdPartySignInUp.ts +++ b/packages/user/src/supertokens/recipes/config/third-party-email-password/thirdPartySignInUp.ts @@ -1,6 +1,5 @@ import { deleteUser } from "supertokens-node"; import { getUserByThirdPartyInfo } from "supertokens-node/recipe/thirdpartyemailpassword"; -import UserRoles from "supertokens-node/recipe/userroles"; import areRolesExist from "../../../utils/areRolesExist"; @@ -11,7 +10,7 @@ const thirdPartySignInUp = ( originalImplementation: RecipeInterface, fastify: FastifyInstance ): RecipeInterface["thirdPartySignInUp"] => { - const { config, log } = fastify; + const { config, log, slonik } = fastify; return async (input) => { const roles = (input.userContext.roles || []) as string[]; @@ -34,32 +33,23 @@ const thirdPartySignInUp = ( input ); - if (originalResponse.status === "OK" && originalResponse.createdNewUser) { - if (!(await areRolesExist(roles))) { - await deleteUser(originalResponse.user.id); + if ( + originalResponse.status === "OK" && + originalResponse.createdNewUser && + !(await areRolesExist(roles, config, slonik)) + ) { + await deleteUser(originalResponse.user.id); - log.error(`At least one role from ${roles.join(", ")} does not exist.`); + log.error(`At least one role from ${roles.join(", ")} does not exist.`); - throw { - name: "SIGN_UP_FAILED", - message: "Something went wrong", - statusCode: 500, - } as FastifyError; - } - - for (const role of roles) { - const rolesResponse = await UserRoles.addRoleToUser( - originalResponse.user.id, - role - ); - - if (rolesResponse.status !== "OK") { - log.error(rolesResponse.status); - } - } + throw { + name: "SIGN_UP_FAILED", + message: "Something went wrong", + statusCode: 500, + } as FastifyError; } - return originalResponse; + return { ...originalResponse, roles }; }; }; diff --git a/packages/user/src/supertokens/recipes/config/third-party-email-password/thirdPartySignInUpPost.ts b/packages/user/src/supertokens/recipes/config/third-party-email-password/thirdPartySignInUpPost.ts index c9b8995fa..618f971e5 100644 --- a/packages/user/src/supertokens/recipes/config/third-party-email-password/thirdPartySignInUpPost.ts +++ b/packages/user/src/supertokens/recipes/config/third-party-email-password/thirdPartySignInUpPost.ts @@ -2,6 +2,7 @@ import { formatDate } from "@dzangolab/fastify-slonik"; import { deleteUser } from "supertokens-node"; import { ROLE_USER } from "../../../../constants"; +import getRolesByNames from "../../../../lib/getRolesByNames"; import getUserService from "../../../../lib/getUserService"; import type { User } from "../../../../types"; @@ -40,7 +41,18 @@ const thirdPartySignInUpPOST = ( throw new Error("User not found"); } - user.roles = input.userContext.roles; + const rolesResponse = await getRolesByNames( + input.userContext.roles, + config, + slonik + ); + + const rolesIds = rolesResponse.map(({ id }) => id); + + await userService.addRolesToUser(originalResponse.user.id, rolesIds); + + user = (await userService.findById(originalResponse.user.id)) as User; + /*eslint-disable-next-line @typescript-eslint/no-explicit-any */ } catch (error: any) { log.error("Error while creating user"); @@ -84,14 +96,11 @@ const thirdPartySignInUpPOST = ( } return { - status: "OK", - createdNewUser: originalResponse.createdNewUser, + ...originalResponse, user: { ...originalResponse.user, ...user, }, - session: originalResponse.session, - authCodeResponse: originalResponse.authCodeResponse, }; } diff --git a/packages/user/src/supertokens/recipes/config/userRolesRecipeConfig.ts b/packages/user/src/supertokens/recipes/config/userRolesRecipeConfig.ts deleted file mode 100644 index 2dbf2df9e..000000000 --- a/packages/user/src/supertokens/recipes/config/userRolesRecipeConfig.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { TypeInput as UserRolesRecipeConfig } from "supertokens-node/recipe/userroles/types"; - -const getUserRolesRecipeConfig = (): UserRolesRecipeConfig => { - return {}; -}; - -export default getUserRolesRecipeConfig; diff --git a/packages/user/src/supertokens/recipes/index.ts b/packages/user/src/supertokens/recipes/index.ts index 46d702268..28e1f054d 100644 --- a/packages/user/src/supertokens/recipes/index.ts +++ b/packages/user/src/supertokens/recipes/index.ts @@ -1,7 +1,6 @@ import initEmailVerificationRecipe from "./initEmailVerificationRecipe"; import initSessionRecipe from "./initSessionRecipe"; import initThirdPartyEmailPassword from "./initThirdPartyEmailPasswordRecipe"; -import initUserRolesRecipe from "./initUserRolesRecipe"; import type { FastifyInstance } from "fastify"; import type { RecipeListFunction } from "supertokens-node/types"; @@ -10,7 +9,6 @@ const getRecipeList = (fastify: FastifyInstance): RecipeListFunction[] => { const recipeList = [ initSessionRecipe(fastify), initThirdPartyEmailPassword(fastify), - initUserRolesRecipe(fastify), ]; if (fastify.config.user.features?.signUp?.emailVerification) { diff --git a/packages/user/src/supertokens/recipes/initUserRolesRecipe.ts b/packages/user/src/supertokens/recipes/initUserRolesRecipe.ts deleted file mode 100644 index b2b1f412c..000000000 --- a/packages/user/src/supertokens/recipes/initUserRolesRecipe.ts +++ /dev/null @@ -1,18 +0,0 @@ -import UserRoles from "supertokens-node/recipe/userroles"; - -import getUserRolesRecipeConfig from "./config/userRolesRecipeConfig"; - -import type { SupertokensRecipes } from "../types"; -import type { FastifyInstance } from "fastify"; - -const init = (fastify: FastifyInstance) => { - const recipes = fastify.config.user.supertokens.recipes as SupertokensRecipes; - - if (recipes && recipes.userRoles) { - return UserRoles.init(recipes.userRoles(fastify)); - } - - return UserRoles.init(getUserRolesRecipeConfig()); -}; - -export default init; diff --git a/packages/user/src/supertokens/types/index.ts b/packages/user/src/supertokens/types/index.ts index 4eabbb2b7..dc824fe75 100644 --- a/packages/user/src/supertokens/types/index.ts +++ b/packages/user/src/supertokens/types/index.ts @@ -8,7 +8,6 @@ import type { TypeInput as EmailVerificationRecipeConfig } from "supertokens-nod import type { TypeInput as SessionRecipeConfig } from "supertokens-node/recipe/session/types"; import type { TypeProvider } from "supertokens-node/recipe/thirdpartyemailpassword"; import type { TypeInput as ThirdPartyEmailPasswordRecipeConfig } from "supertokens-node/recipe/thirdpartyemailpassword/types"; -import type { TypeInput as UserRolesRecipeConfig } from "supertokens-node/recipe/userroles/types"; const { Apple, Facebook, Github, Google } = ThirdPartyEmailPassword; @@ -17,7 +16,6 @@ interface SupertokensRecipes { | EmailVerificationRecipe | ((fastify: FastifyInstance) => EmailVerificationRecipeConfig); session?: SessionRecipe | ((fastify: FastifyInstance) => SessionRecipeConfig); - userRoles?: (fastify: FastifyInstance) => UserRolesRecipeConfig; thirdPartyEmailPassword?: | ThirdPartyEmailPasswordRecipe | ((fastify: FastifyInstance) => ThirdPartyEmailPasswordRecipeConfig); diff --git a/packages/user/src/supertokens/utils/areRolesExist.ts b/packages/user/src/supertokens/utils/areRolesExist.ts index b6cb6b042..71722215c 100644 --- a/packages/user/src/supertokens/utils/areRolesExist.ts +++ b/packages/user/src/supertokens/utils/areRolesExist.ts @@ -1,9 +1,28 @@ -import UserRoles from "supertokens-node/recipe/userroles"; +import RoleService from "../../model/roles/service"; -const areRolesExist = async (roles: string[]): Promise => { - const { roles: allRoles } = await UserRoles.getAllRoles(); +import type { Role, RoleCreateInput, RoleUpdateInput } from "../../types"; +import type { ApiConfig } from "@dzangolab/fastify-config"; +import type { Database } from "@dzangolab/fastify-slonik"; - return roles.every((role) => allRoles.includes(role)); +const areRolesExist = async ( + roles: string[], + config: ApiConfig, + slonik: Database, + dbSchema?: string +): Promise => { + const service = new RoleService( + config, + slonik, + dbSchema + ); + + const count = await service.count({ + key: "role", + operator: "in", + value: roles.join(","), + }); + + return !!count; }; export default areRolesExist; diff --git a/packages/user/src/supertokens/utils/isRoleExists.ts b/packages/user/src/supertokens/utils/isRoleExists.ts index 9ac253176..9bb548c8d 100644 --- a/packages/user/src/supertokens/utils/isRoleExists.ts +++ b/packages/user/src/supertokens/utils/isRoleExists.ts @@ -1,9 +1,28 @@ -import UserRoles from "supertokens-node/recipe/userroles"; +import RoleService from "../../model/roles/service"; -const isRoleExists = async (role: string): Promise => { - const { roles } = await UserRoles.getAllRoles(); +import type { Role, RoleCreateInput, RoleUpdateInput } from "../../types"; +import type { ApiConfig } from "@dzangolab/fastify-config"; +import type { Database } from "@dzangolab/fastify-slonik"; - return roles.includes(role); +const isRoleExists = async ( + role: string, + config: ApiConfig, + slonik: Database, + dbSchema?: string +): Promise => { + const service = new RoleService( + config, + slonik, + dbSchema + ); + + const count = await service.count({ + key: "role", + operator: "eq", + value: role, + }); + + return !!count; }; export default isRoleExists; diff --git a/packages/user/src/userContext.ts b/packages/user/src/userContext.ts index 5b976ef91..584419ef4 100644 --- a/packages/user/src/userContext.ts +++ b/packages/user/src/userContext.ts @@ -2,7 +2,6 @@ import mercurius from "mercurius"; import { wrapResponse } from "supertokens-node/framework/fastify"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; import Session from "supertokens-node/recipe/session"; -import UserRoles from "supertokens-node/recipe/userroles"; import getUserService from "./lib/getUserService"; @@ -64,10 +63,8 @@ const userContext = async ( throw new Error("Unable to find user"); } - const { roles } = await UserRoles.getRolesForUser(userId); - context.user = user; - context.roles = roles; + context.roles = user.roles; } };