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 28830e311..9d3c11e0f 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 @@ -73,8 +73,6 @@ const emailPasswordSignUp = ( }; } - user.roles = roles; - originalResponse.user = { ...originalResponse.user, ...user, diff --git a/packages/user/src/model/users/service.ts b/packages/user/src/model/users/service.ts index 7b8189613..d2d2fb441 100644 --- a/packages/user/src/model/users/service.ts +++ b/packages/user/src/model/users/service.ts @@ -18,29 +18,15 @@ class UserService< // eslint-disable-next-line prettier/prettier implements Service { - get table() { - return this.config.user?.table?.name || TABLE_USERS; - } + addRolesToUser = async (id: string | number, roleIds: number[]) => { + const query = this.factory.getAddRolesToUserSql(id, roleIds); - get factory() { - if (!this.table) { - throw new Error(`Service table is not defined`); - } - - if (!this._factory) { - this._factory = new UserSqlFactory< - User, - UserCreateInput, - UserUpdateInput - >(this); - } + const result = await this.database.connect((connection) => { + return connection.any(query); + }); - return this._factory as UserSqlFactory< - User, - UserCreateInput, - UserUpdateInput - >; - } + return result; + }; changePassword = async ( userId: string, @@ -104,6 +90,42 @@ class UserService< }; } }; + + isAdminExists = async (): Promise => { + const query = this.factory.getIsAdminExistsSql(); + + const result = await this.database.connect(async (connection) => { + const columns = await connection.one(query); + + return columns.isAdminExists; + }); + + return result as boolean; + }; + + get table() { + return this.config.user?.table?.name || TABLE_USERS; + } + + get factory() { + if (!this.table) { + throw new Error(`Service table is not defined`); + } + + if (!this._factory) { + this._factory = new UserSqlFactory< + User, + UserCreateInput, + UserUpdateInput + >(this); + } + + return this._factory as UserSqlFactory< + User, + UserCreateInput, + UserUpdateInput + >; + } } export default UserService; diff --git a/packages/user/src/model/users/sqlFactory.ts b/packages/user/src/model/users/sqlFactory.ts index 31ffd273c..e047741db 100644 --- a/packages/user/src/model/users/sqlFactory.ts +++ b/packages/user/src/model/users/sqlFactory.ts @@ -3,17 +3,26 @@ import { createLimitFragment, createFilterFragment, createTableIdentifier, + createTableFragment, } from "@dzangolab/fastify-slonik"; import humps from "humps"; -import { QueryResultRow, QuerySqlToken, sql } from "slonik"; +import { sql } from "slonik"; +import { z } from "zod"; import { createSortFragment, createSortRoleFragment } from "./sql"; +import { + ROLE_ADMIN, + ROLE_SUPERADMIN, + TABLE_ROLES, + TABLE_USER_ROLES, +} from "../../constants"; import type { SqlFactory, FilterInput, SortInput, } from "@dzangolab/fastify-slonik"; +import type { QueryResultRow, QuerySqlToken } from "slonik"; /* eslint-disable brace-style */ class UserSqlFactory< @@ -25,6 +34,21 @@ class UserSqlFactory< implements SqlFactory { /* eslint-enabled */ + getAddRolesToUserSql = ( + id: number | string, + roleIds: number[] + ): QuerySqlToken => { + return sql.unsafe` + INSERT INTO ${this.userRolesIdentifier} ("user_id", "role_id") + SELECT * + FROM ${sql.unnest( + roleIds.map((roleId) => { + return [id, roleId]; + }), + ["varchar", "int4"] + )} ON CONFLICT DO NOTHING; + `; + }; getFindByIdSql = (id: number | string): QuerySqlToken => { return sql.type(this.validationSchema)` @@ -33,16 +57,36 @@ class UserSqlFactory< COALESCE(user_role.role, '[]') AS roles FROM ${this.getTableFragment()} LEFT JOIN LATERAL ( - SELECT jsonb_agg(ur.role ${createSortRoleFragment( - sql.identifier(["ur", "role"]) + SELECT jsonb_agg(r ${createSortRoleFragment( + sql.identifier(["r", "id"]) )}) AS role - FROM "public"."st__user_roles" as ur + FROM ${this.userRolesIdentifier} as ur + JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = users.id ) AS user_role ON TRUE WHERE id = ${id}; `; }; + getIsAdminExistsSql = () => { + const schema = z.object({ + isAdminExists: z.boolean(), + }); + + return sql.type(schema)` + SELECT EXISTS ( + SELECT 1 + FROM ${this.getTableFragment()} u + INNER JOIN ${this.userRolesIdentifier} ur ON u.id = ur.user_id + INNER JOIN ${this.rolesIdentifier} r ON ur.role_id = r.id + WHERE r.role IN (${sql.join( + [ROLE_ADMIN, ROLE_SUPERADMIN], + sql.fragment`, ` + )}) + ) as is_admin_exists; + `; + }; + getListSql = ( limit: number, offset?: number, @@ -57,11 +101,12 @@ class UserSqlFactory< COALESCE(user_role.role, '[]') AS roles FROM ${this.getTableFragment()} LEFT JOIN LATERAL ( - SELECT jsonb_agg(ur.role ${createSortRoleFragment( - sql.identifier(["ur", "role"]), + SELECT jsonb_agg(r ${createSortRoleFragment( + sql.identifier(["r", "id"]), sort )}) AS role - FROM "public"."st__user_roles" as ur + FROM ${this.userRolesIdentifier} as ur + JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = users.id ) AS user_role ON TRUE ${createFilterFragment(filters, tableIdentifier)} @@ -91,16 +136,25 @@ class UserSqlFactory< SELECT COALESCE(user_role.role, '[]') AS roles FROM ${this.getTableFragment()} LEFT JOIN LATERAL ( - SELECT jsonb_agg(ur.role ${createSortRoleFragment( - sql.identifier(["ur", "role"]) + SELECT jsonb_agg(r ${createSortRoleFragment( + sql.identifier(["r", "id"]) )}) AS role - FROM "public"."st__user_roles" as ur + FROM ${this.userRolesIdentifier} as ur + JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = users.id ) AS user_role ON TRUE WHERE id = ${id} ) as roles; `; }; + + get rolesIdentifier() { + return createTableFragment(TABLE_ROLES); + } + + get userRolesIdentifier() { + return createTableIdentifier(TABLE_USER_ROLES); + } } export default UserSqlFactory; 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 9bbb3a788..dac59697f 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 @@ -62,8 +62,6 @@ const emailPasswordSignUp = ( }; } - user.roles = roles; - originalResponse.user = { ...originalResponse.user, ...user, diff --git a/packages/user/src/types/index.ts b/packages/user/src/types/index.ts index 8103cc06a..a9fdc3915 100644 --- a/packages/user/src/types/index.ts +++ b/packages/user/src/types/index.ts @@ -1,3 +1,4 @@ +import type { Role } from "./roles"; import type { PaginatedList } from "@dzangolab/fastify-slonik"; import type { MercuriusContext } from "mercurius"; import type { QueryResultRow } from "slonik"; @@ -33,7 +34,7 @@ interface User { disabled: boolean; email: string; lastLoginAt: number; - roles?: string[]; + roles: Omit[]; signedUpAt: number; }