diff --git a/tdrive/backend/node/src/services/user/services/users/service.ts b/tdrive/backend/node/src/services/user/services/users/service.ts index 818979b7..ac361266 100644 --- a/tdrive/backend/node/src/services/user/services/users/service.ts +++ b/tdrive/backend/node/src/services/user/services/users/service.ts @@ -31,6 +31,8 @@ import { isNumber, isString } from "lodash"; import NodeCache from "node-cache"; import gr from "../../../global-resolver"; import { TYPE as DriveFileType, DriveFile } from "../../../documents/entities/drive-file"; +import { UpdateUser } from "./types"; +import { formatUsername } from "../../../../utils/users"; export class UserServiceImpl { version: "1"; @@ -77,14 +79,29 @@ export class UserServiceImpl { } } + private async updateExtRepositoryInCaseChangeEmail(user: User, context?: ExecutionContext) { + if (user.identity_provider_id === "null") { + return; + } + + const extUser = await this.extUserRepository.findOne({ user_id: user.id }, {}, context); + if (extUser) { + await this.extUserRepository.remove(extUser, context); + } + + const newExtUser = getExternalUserInstance({ + service_id: user.identity_provider || "null", + external_id: user.identity_provider_id || "null", + user_id: user.id, + }); + await this.extUserRepository.save(newExtUser, context); + } + private assignDefaults(user: User) { user.creation_date = !isNumber(user.creation_date) ? Date.now() : user.creation_date; if (user.identity_provider_id && !user.identity_provider) user.identity_provider = "console"; if (user.email_canonical) user.email_canonical = user.email_canonical.toLocaleLowerCase(); - if (user.username_canonical) - user.username_canonical = (user.username_canonical || "") - .toLocaleLowerCase() - .replace(/[^a-z0-9_-]/, ""); + if (user.username_canonical) user.username_canonical = formatUsername(user.username_canonical); } async create(user: User, context?: ExecutionContext): Promise> { @@ -92,8 +109,35 @@ export class UserServiceImpl { return new CreateResult("user", user); } - update(pk: Partial, item: User, context?: ExecutionContext): Promise> { - throw new Error("Method not implemented."); + async update( + instance: User, + user: UpdateUser, + context?: ExecutionContext, + ): Promise> { + let isChangeEmail = false; + if (instance.email_canonical !== user.email) { + isChangeEmail = true; + } + + instance.email_canonical = user.email || instance.email_canonical; + instance.first_name = user.first_name || instance.first_name; + instance.last_name = user.last_name || instance.last_name; + instance.picture = user.picture || instance.picture; + instance.creation_date = !isNumber(instance.creation_date) + ? Date.now() + : instance.creation_date; + if (isChangeEmail) { + instance.username_canonical = formatUsername(user.email); + instance.identity_provider_id = + instance.identity_provider_id !== "null" ? user.email : "null"; + } + + await this.repository.save(instance, context); + if (isChangeEmail) { + await this.updateExtRepositoryInCaseChangeEmail(instance, context); + } + + return new UpdateResult("user", instance); } async save(user: User, context?: ExecutionContext): Promise> { diff --git a/tdrive/backend/node/src/services/user/services/users/types.ts b/tdrive/backend/node/src/services/user/services/users/types.ts index 18a8d752..5c63854a 100644 --- a/tdrive/backend/node/src/services/user/services/users/types.ts +++ b/tdrive/backend/node/src/services/user/services/users/types.ts @@ -8,3 +8,10 @@ export type SearchUserOptions = { workspaceId?: string; channelId?: string; }; + +export type UpdateUser = { + email: string; + picture: string; + first_name: string; + last_name: string; +}; diff --git a/tdrive/backend/node/src/services/user/web/controller.ts b/tdrive/backend/node/src/services/user/web/controller.ts index d13283aa..52c07789 100644 --- a/tdrive/backend/node/src/services/user/web/controller.ts +++ b/tdrive/backend/node/src/services/user/web/controller.ts @@ -37,6 +37,7 @@ import { formatUser } from "../../../utils/users"; import gr from "../../global-resolver"; import config from "config"; import { getLogger } from "../../../core/platform/framework"; +import { UpdateUser } from "../services/users/types"; export class UsersCrudController implements @@ -366,6 +367,35 @@ export class UsersCrudController used: quota, } as UserQuota; } + + async update( + request: FastifyRequest<{ Body: UpdateUser; Params: UserParameters }>, + reply: FastifyReply, + ): Promise> { + const context = getExecutionContext(request); + + const id = request.params.id; + const user = await gr.services.users.get({ id }); + if (!user) { + reply.notFound(`User ${id} not found`); + return; + } + + const body = { ...request.body }; + if (user.email_canonical !== body.email) { + const userByEmail = await gr.services.users.getByEmail(body.email, context); + if (userByEmail) { + reply.conflict(`Email ${body.email} is existed`); + return; + } + } + + await gr.services.users.update(user, body, context); + + return { + resource: await formatUser(user), + }; + } } function getExecutionContext(request: FastifyRequest): ExecutionContext { diff --git a/tdrive/backend/node/src/services/user/web/routes.ts b/tdrive/backend/node/src/services/user/web/routes.ts index 6937e816..c42625c9 100644 --- a/tdrive/backend/node/src/services/user/web/routes.ts +++ b/tdrive/backend/node/src/services/user/web/routes.ts @@ -143,6 +143,13 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, options, next) handler: usersController.recent.bind(usersController), }); + fastify.route({ + method: "PUT", + url: `${usersUrl}/:id`, + preValidation: [fastify.authenticateOptional], + handler: usersController.update.bind(usersController), + }); + next(); }; diff --git a/tdrive/backend/node/src/utils/users.ts b/tdrive/backend/node/src/utils/users.ts index 0a631384..6ef561ec 100644 --- a/tdrive/backend/node/src/utils/users.ts +++ b/tdrive/backend/node/src/utils/users.ts @@ -87,3 +87,7 @@ export async function formatUser( return resUser; } + +export function formatUsername(value: string) { + return (value?.replace(/[^a-z0-9_-]/, "") || "").toLocaleLowerCase(); +} diff --git a/tdrive/backend/node/test/e2e/users/users.spec.ts b/tdrive/backend/node/test/e2e/users/users.spec.ts index 2f116699..d91fecde 100644 --- a/tdrive/backend/node/test/e2e/users/users.spec.ts +++ b/tdrive/backend/node/test/e2e/users/users.spec.ts @@ -51,12 +51,11 @@ describe("The /users API", () => { username: "adminuser", firstName: "admin", }); - await testDbService.createUser([workspacePk]); - + await testDbService.createUser([workspacePk], { password: "123456" }); }); - afterAll(async () => { - }); + // eslint-disable-next-line @typescript-eslint/no-empty-function + afterAll(async () => {}); describe("The GET /users/:id route", () => { it("should 401 when not authenticated", async () => { @@ -136,7 +135,6 @@ describe("The /users API", () => { }), ]), ); - }); it("should 200 and short response for another user", async () => { @@ -222,7 +220,7 @@ describe("The /users API", () => { }, query: { search: "anon", - company_ids: oneUser.workspace.company_id + company_ids: oneUser.workspace.company_id, }, }); @@ -232,9 +230,7 @@ describe("The /users API", () => { const resources = json.resources; console.log(resources); expect(resources.length).toBe(0); - }); - }); describe("The GET /users/:user_id/companies route", () => { @@ -354,7 +350,6 @@ describe("The /users API", () => { total_guests: expect.any(Number), total_messages: expect.any(Number), }); - }); }); @@ -473,7 +468,6 @@ describe("The /users API", () => { user = await testDbService.getUserFromDb({ id: firstId }); expect(user.devices).toMatchObject([]); - }); }); describe("List registered devices (GET)", () => { @@ -545,8 +539,94 @@ describe("The /users API", () => { expect(user.devices).toMatchObject([]); const device = await testDbService.getDeviceFromDb(deviceToken); expect(device).toBeFalsy(); - }); }); }); + + describe("The PUT /users/:id route", () => { + it("should 200 and response for updated user", async () => { + const myId = testDbService.users[0].id; + const updatedUserId = testDbService.users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: myId }); + const response = await platform.app.inject({ + method: "PUT", + url: `${url}/users/${updatedUserId}`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + body: { + email: "newvalue@gmail.com", + first_name: "New", + last_name: "Value", + picture: "null", + }, + }); + + expect(response.statusCode).toBe(200); + + const resource = response.json()["resource"]; + + console.log("resource: ", resource); + + expect(resource).toMatchObject({ + id: updatedUserId, + provider: expect.any(String), + provider_id: expect.any(String), + email: expect.any(String), + username: expect.any(String), + is_verified: expect.any(Boolean), + picture: expect.any(String), + first_name: expect.any(String), + last_name: expect.any(String), + full_name: expect.any(String), + created_at: expect.any(Number), + deleted: expect.any(Boolean), + status: null, + cache: { companies: expect.any(Array) }, + }); + + // Separate check for last_activity + expect([null, expect.any(Number)]).toContainEqual(resource.last_activity); + + expect(resource).not.toMatchObject({ + locale: expect.anything(), + timezone: expect.anything(), + companies: expect.anything(), + }); + }); + + it("should login with new email", async () => { + const myId = testDbService.users[0].id; + const updatedUserId = testDbService.users[1].id; + + const jwtToken = await platform.auth.getJWTToken({ sub: myId }); + await platform.app.inject({ + method: "PUT", + url: `${url}/users/${updatedUserId}`, + headers: { + authorization: `Bearer ${jwtToken}`, + }, + body: { + email: "newvalue@gmail.com", + first_name: "New", + last_name: "Value", + picture: "null", + }, + }); + + const response = await platform.app.inject({ + method: "POST", + url: "/internal/services/console/v1/login", + body: { + email: "newvalue@gmail.com", + password: "123456", + remember_me: true, + device: {}, + }, + }); + + expect(response.statusCode).toBe(200); + }); + }); });