diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index ab79319ed..1fe0b2a2a 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -30,6 +30,7 @@ jobs: OVH_APP_KEY: ${{ secrets.OVH_APP_KEY }} OVH_APP_SECRET: ${{ secrets.OVH_APP_SECRET }} OVH_CONSUMER_KEY: ${{ secrets.OVH_CONSUMER_KEY }} + PORT: 8100 PROTECTED_API_KEYS: test-api-key - name: Wait uses: cygnetdigital/wait_for_response@v2.0.0 diff --git a/.talismanrc b/.talismanrc index 23f02d7c3..33fbabf3e 100644 --- a/.talismanrc +++ b/.talismanrc @@ -5,6 +5,8 @@ fileignoreconfig: checksum: 07171a40047c38771624063fb61511998d3961815c3dceb135a1ee6d540f970d - filename: .github/workflows/nodejs.yml checksum: bf0ae31737daf27be68b7d2c4e63e1ef14cb799f1853462fdadcf2422ddd53a2 +- filename: __tests__/test-user.ts + checksum: 855d1520858d599dd95ebfd6b9845ad2f5df76307915568b8f5b558cec12a9b1 - filename: .github/workflows/playwright.yml checksum: 38ba47ef852e85e51e9e97d635c6b55b63db8940a3c33f2546fa8386c3311a3d - filename: __tests__/test-worker.ts @@ -27,12 +29,24 @@ fileignoreconfig: checksum: 685b3f177bb6124655bb439beb6f3cb4ecdbbeb62adeab9e1f56e811a09a9f5b - filename: src/app/api/member/[username]/route.ts checksum: a5371c646bc9955b72b6cdb5ee3aef340004a092d9e70bbbe4b308c940880370 +- filename: src/app/api/member/actions/createRedirectionForUser.ts + checksum: 12c54f94e0d66d8e3d51b9e9dd7032a1757c50fc85733b29befe9c0162fe3251 +- filename: src/app/api/member/actions/deleteRedirectionForUser.ts + checksum: deddb6b63051856b44a031e097f4614a1ff42d97ac49f1bc1d21f28ed377d7d1 +- filename: src/app/api/member/actions/updatePasswordForUser.ts + checksum: f0f76c8fc4094fb1a0ae2fa456dcf72b0d2481dbe4cfd8632241306d9b63c2d3 - filename: src/app/api/services/actions.ts checksum: 80f4cffcc5861befdb98c676df064595dae5c318349c45059ba87fe20e96f2a2 - filename: src/components/CommunityPage/Community.tsx checksum: 50ed5a9ba8e03d4ffc248026cfae6fd6331c84c21b30aaa73c0bf30f22be9e2c - filename: src/components/IncubatorForm/IncubatorForm.tsx checksum: 267f0f0e491b06862cd957ae86d22baf799a705370673cd24e588391a4613a5a +- filename: src/components/MemberPage/Email/BlocChangerMotDePasse.tsx + checksum: 9ee37641aebd9381fccbe0b967e5f2b8a546760824e62ed3a564f50752fdae1f +- filename: src/components/MemberPage/Email/EmailContainer.tsx + checksum: 2c46d4aa4dc8caece9bc7f377178e855562c5321d2b87b24ff32cb307fe6946b +- filename: src/components/MemberPage/Email/WebMailButtons.tsx + checksum: da7b71f65b7519ea53e594910c24d14e3579d32ab1cb9144f32fa81a71c0a497 - filename: src/components/IncubatorPage/IncubatorPage.tsx checksum: 61c912058a8f8c12b1173a37b7fa555c11e2ddbf36bd95d0e986576c9ef5e79a - filename: src/components/MemberPage/EmailUpgrade.tsx diff --git a/__tests__/test-admin.ts b/__tests__/test-admin.ts index 54f248962..2da1cb5ce 100644 --- a/__tests__/test-admin.ts +++ b/__tests__/test-admin.ts @@ -1,9 +1,14 @@ -import chai from "chai"; +import chai, { assert, expect } from "chai"; import chaiHttp from "chai-http"; +import * as nextAuth from "next-auth/next"; import sinon from "sinon"; import testUsers from "./users.json"; import utils from "./utils"; +import { getMattermostInfo } from "@/app/api/admin/actions/getMattermostAdmin"; +import { getMattermostUsersInfo } from "@/app/api/admin/actions/getMattermostUsersInfo"; +import { sendMessageToUsersOnChat } from "@/app/api/admin/actions/sendMattermostMessage"; +import { db } from "@/lib/kysely"; import routes from "@/routes/routes"; import config from "@/server/config"; import * as adminConfig from "@/server/config/admin.config"; @@ -16,221 +21,240 @@ import * as mattermostScheduler from "@schedulers/mattermostScheduler/removeBeta chai.use(chaiHttp); describe("Test Admin", () => { - describe("GET /admin unauthenticated", () => { - it("should redirect to login", (done) => { - chai.request(app) - .get("/api/admin") - .redirects(0) - .end((err, res) => { - res.should.have.status(500); - done(); - }); - }); - }); + describe("Test admin mattermost server action", () => { + describe("Test admin mattermost server action api if user is not admin", () => { + let getServerSessionStub; + let isPublicServiceEmailStub; + let user; - // describe("GET /admin authenticated", () => { - // let getToken; + beforeEach(async () => { + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves({}); - // beforeEach(() => { - // getToken = sinon.stub(session, "getToken"); - // getToken.returns(utils.getJWT("membre.actif")); - // }); + await utils.createUsers(testUsers); + user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.actif") + .executeTakeFirstOrThrow(); + const mockSession = { + user: { + id: "membre.actif", + isAdmin: false, + uuid: user.uuid, + }, + }; + getServerSessionStub.resolves(mockSession); + }); + afterEach(async () => { + sinon.restore(); + await utils.deleteUsers(testUsers); + }); - // afterEach(() => { - // getToken.restore(); - // }); - // it("should return a valid page", (done) => { - // chai.request(app) - // .get("/api/admin") - // .end((err, res) => { - // res.should.have.status(200); - // done(); - // }); - // }); - // }); + it("should return a forbidden error if user not in admin", async () => { + try { + await getMattermostInfo(); + } catch (err) { + assert(err instanceof Error); + assert.strictEqual( + err.message, + "L'utilisateur doit être administrateur" + ); + } + }); + it("should return a forbidden error if user not in admin", async () => { + try { + await getMattermostUsersInfo({ + fromBeta: true, + }); + } catch (err) { + assert(err instanceof Error); + assert.strictEqual( + err.message, + "L'utilisateur doit être administrateur" + ); + } + }); + it("should return a forbidden error if user not in admin", async () => { + try { + await sendMessageToUsersOnChat({ + text: "toto", + fromBeta: false, + }); + } catch (err) { + assert(err instanceof Error); + assert.strictEqual( + err.message, + "L'utilisateur doit être administrateur" + ); + } + }); + }); + describe("Test admin mattermost server action api if user is admin", () => { + let getServerSessionStub; + let user; - describe("GET /admin/mattermost authenticated", () => { - let getToken; + beforeEach(async () => { + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves({}); - beforeEach(async () => { - getToken = sinon.stub(session, "getToken"); - getToken.returns(utils.getJWT("membre.actif")); - await utils.createUsers(testUsers); - }); + await utils.createUsers(testUsers); + user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.actif") + .executeTakeFirstOrThrow(); + const mockSession = { + user: { + id: "membre.actif", + isAdmin: true, + uuid: user.uuid, + }, + }; + getServerSessionStub.resolves(mockSession); + }); + afterEach(async () => { + sinon.restore(); + await utils.deleteUsers(testUsers); + }); - afterEach(async () => { - getToken.restore(); - await utils.deleteUsers(testUsers); - }); - it("should return a forbidden error if user not in admin", async () => { - const res = await chai - .request(app) - .get(routes.ADMIN_MATTERMOST_API); - res.should.have.status(403); - }); - it("should return a forbidden error if user not in admin", async () => { - const res = await chai - .request(app) - .get(routes.ADMIN_MATTERMOST_MESSAGE_API); - res.should.have.status(403); - }); - it("should return a forbidden error if user not in admin", async () => { - const res = await chai - .request(app) - .post(routes.ADMIN_MATTERMOST_SEND_MESSAGE); - res.should.have.status(403); - }); - it("should return /api/admin/mattermost page if user is admin", async () => { - const getAdminStub = sinon - .stub(adminConfig, "getAdmin") - .returns(["membre.actif"]); - const getMattermostUsersWithStatus = sinon - .stub(mattermostScheduler, "getMattermostUsersWithStatus") - .returns(Promise.resolve([])); - const res = await chai - .request(app) - .get(routes.ADMIN_MATTERMOST_API); - res.should.have.status(200); - getAdminStub.restore(); - getMattermostUsersWithStatus.restore(); - }); - it("should return /admin/send-message page if user is admin", async () => { - const getAdminStub = sinon - .stub(adminConfig, "getAdmin") - .returns(["membre.actif"]); - const getMattermostUsersWithStatus = sinon - .stub(mattermostScheduler, "getMattermostUsersWithStatus") - .returns(Promise.resolve([])); - const getUserWithParams = sinon.stub(chat, "getUserWithParams"); - const sendInfoToChat = sinon.stub(chat, "sendInfoToChat"); - getUserWithParams.onCall(0).returns([ - { - username: "membre.actif", - email: `membre.actif@${config.domain}`, - }, - ]); - getUserWithParams.onCall(1).returns([]); - const res = await chai - .request(app) - .post(routes.ADMIN_MATTERMOST_SEND_MESSAGE) - .send({ + it("should return /api/admin/mattermost page if user is admin", async () => { + const getAdminStub = sinon + .stub(adminConfig, "getAdmin") + .returns(["membre.actif"]); + const getMattermostUsersWithStatus = sinon + .stub(mattermostScheduler, "getMattermostUsersWithStatus") + .returns(Promise.resolve([])); + const res = await getMattermostInfo(); + getAdminStub.restore(); + getMattermostUsersWithStatus.restore(); + }); + it("should return /admin/send-message page if user is admin", async () => { + const getAdminStub = sinon + .stub(adminConfig, "getAdmin") + .returns(["membre.actif"]); + const getMattermostUsersWithStatus = sinon + .stub(mattermostScheduler, "getMattermostUsersWithStatus") + .returns(Promise.resolve([])); + const getUserWithParams = sinon.stub(chat, "getUserWithParams"); + const sendInfoToChat = sinon.stub(chat, "sendInfoToChat"); + getUserWithParams.onCall(0).returns([ + { + username: "membre.actif", + email: `membre.actif@${config.domain}`, + }, + ]); + getUserWithParams.onCall(1).returns([]); + await sendMessageToUsersOnChat({ fromBeta: true, - excludeEmails: "", - includeEmails: "", text: "", }); - res.should.have.status(200); - sendInfoToChat.calledOnce.should.be.true; - getUserWithParams.callCount.should.be.eq(0); - getAdminStub.restore(); - getUserWithParams.restore(); - getMattermostUsersWithStatus.restore(); - sendInfoToChat.restore(); - }); - it("should send message to all users if prod is true and channel undefined", async () => { - const getAdminStub = sinon - .stub(adminConfig, "getAdmin") - .returns(["membre.actif"]); - const getMattermostUsersWithStatus = sinon - .stub(mattermostScheduler, "getMattermostUsersWithStatus") - .returns(Promise.resolve([])); - const getUserWithParams = sinon.stub(chat, "getUserWithParams"); - const sendInfoToChat = sinon.stub(chat, "sendInfoToChat"); - getUserWithParams.onCall(0).returns([ - { - username: "membre.actif", - email: `membre.actif@${config.domain}`, - }, - ]); - getUserWithParams.onCall(1).returns([]); - const res = await chai - .request(app) - .post(routes.ADMIN_MATTERMOST_SEND_MESSAGE) - .send({ + + sendInfoToChat.calledOnce.should.be.true; + getUserWithParams.callCount.should.be.eq(0); + getAdminStub.restore(); + getUserWithParams.restore(); + getMattermostUsersWithStatus.restore(); + sendInfoToChat.restore(); + }); + it("should send message to all users if prod is true and channel undefined", async () => { + const getAdminStub = sinon + .stub(adminConfig, "getAdmin") + .returns(["membre.actif"]); + const getMattermostUsersWithStatus = sinon + .stub(mattermostScheduler, "getMattermostUsersWithStatus") + .returns(Promise.resolve([])); + const getUserWithParams = sinon.stub(chat, "getUserWithParams"); + const sendInfoToChat = sinon.stub(chat, "sendInfoToChat"); + getUserWithParams.onCall(0).returns([ + { + username: "membre.actif", + email: `membre.actif@${config.domain}`, + }, + ]); + getUserWithParams.onCall(1).returns([]); + await sendMessageToUsersOnChat({ fromBeta: true, - excludeEmails: "", - includeEmails: "", prod: true, + text: "toto", }); - res.should.have.status(200); - getUserWithParams.callCount.should.be.eq(1); - getAdminStub.restore(); - getUserWithParams.restore(); - getMattermostUsersWithStatus.restore(); - sendInfoToChat.restore(); - }); + getUserWithParams.callCount.should.be.eq(1); + getAdminStub.restore(); + getUserWithParams.restore(); + getMattermostUsersWithStatus.restore(); + sendInfoToChat.restore(); + }); - it("should take exclude in consideration", async () => { - const getAdminStub = sinon - .stub(adminConfig, "getAdmin") - .returns(["membre.actif"]); - const getMattermostUsersWithStatus = sinon - .stub(mattermostScheduler, "getMattermostUsersWithStatus") - .returns(Promise.resolve([])); - const getMattermostUsersSpy = sinon.spy( - sendMattermostMessage, - "getMattermostUsers" - ); - const getUserWithParams = sinon.stub(chat, "getUserWithParams"); - const sendInfoToChat = sinon.stub(chat, "sendInfoToChat"); - getUserWithParams.onCall(0).returns([ - { - username: "membre.actif", - email: `membre.actif@${config.domain}`, - }, - ]); - getUserWithParams.onCall(1).returns([]); - const res = await chai - .request(app) - .post(routes.ADMIN_MATTERMOST_SEND_MESSAGE) - .send({ + it("should take exclude in consideration", async () => { + const getAdminStub = sinon + .stub(adminConfig, "getAdmin") + .returns(["membre.actif"]); + const getMattermostUsersWithStatus = sinon + .stub(mattermostScheduler, "getMattermostUsersWithStatus") + .returns(Promise.resolve([])); + const getMattermostUsersSpy = sinon.spy( + sendMattermostMessage, + "getMattermostUsers" + ); + const getUserWithParams = sinon.stub(chat, "getUserWithParams"); + const sendInfoToChat = sinon.stub(chat, "sendInfoToChat"); + getUserWithParams.onCall(0).returns([ + { + username: "membre.actif", + email: `membre.actif@${config.domain}`, + }, + ]); + getUserWithParams.onCall(1).returns([]); + await sendMessageToUsersOnChat({ fromBeta: true, - includeEmails: "", + excludeEmails: [`membre.actif@${config.domain}`], prod: true, + text: "", }); - res.should.have.status(200); - const resMatterUser = await getMattermostUsersSpy.returnValues[0]; - resMatterUser.length.should.be.eq(1); - getUserWithParams.callCount.should.be.eq(1); - getAdminStub.restore(); - getUserWithParams.restore(); - getMattermostUsersWithStatus.restore(); - sendInfoToChat.restore(); - }); + const resMatterUser = await getMattermostUsersSpy + .returnValues[0]; + resMatterUser.length.should.be.eq(0); + getUserWithParams.callCount.should.be.eq(1); + getAdminStub.restore(); + getUserWithParams.restore(); + getMattermostUsersWithStatus.restore(); + sendInfoToChat.restore(); + }); - it("should send message to all users if prod is true and channel set", async () => { - const getAdminStub = sinon - .stub(adminConfig, "getAdmin") - .returns(["membre.actif"]); - const getMattermostUsersWithStatus = sinon - .stub(mattermostScheduler, "getMattermostUsersWithStatus") - .returns(Promise.resolve([])); - const getUserWithParams = sinon.stub(chat, "getUserWithParams"); - const sendInfoToChat = sinon.stub(chat, "sendInfoToChat"); - getUserWithParams.onCall(0).returns([ - { - username: "membre.actif", - email: `membre.actif@${config.domain}`, - }, - ]); - getUserWithParams.onCall(1).returns([]); - const res = await chai - .request(app) - .post(routes.ADMIN_MATTERMOST_SEND_MESSAGE) - .send({ + it("should send message to all users if prod is true and channel set", async () => { + const getAdminStub = sinon + .stub(adminConfig, "getAdmin") + .returns(["membre.actif"]); + const getMattermostUsersWithStatus = sinon + .stub(mattermostScheduler, "getMattermostUsersWithStatus") + .returns(Promise.resolve([])); + const getUserWithParams = sinon.stub(chat, "getUserWithParams"); + const sendInfoToChat = sinon.stub(chat, "sendInfoToChat"); + getUserWithParams.onCall(0).returns([ + { + username: "membre.actif", + email: `membre.actif@${config.domain}`, + }, + ]); + getUserWithParams.onCall(1).returns([]); + await sendMessageToUsersOnChat({ fromBeta: true, - excludeEmails: "", prod: true, channel: "general", + text: "Un super texte", }); - res.should.have.status(200); - getUserWithParams.callCount.should.be.eq(0); - sendInfoToChat.getCall(0).args[0].channel.should.equal("general"); - sendInfoToChat.calledTwice.should.be.true; - getAdminStub.restore(); - getUserWithParams.restore(); - getMattermostUsersWithStatus.restore(); - sendInfoToChat.restore(); + getUserWithParams.callCount.should.be.eq(0); + sendInfoToChat + .getCall(0) + .args[0].channel.should.equal("general"); + sendInfoToChat.calledTwice.should.be.true; + getAdminStub.restore(); + getUserWithParams.restore(); + getMattermostUsersWithStatus.restore(); + sendInfoToChat.restore(); + }); }); }); }); diff --git a/__tests__/test-user.ts b/__tests__/test-user.ts index 0836a4924..b899df77c 100644 --- a/__tests__/test-user.ts +++ b/__tests__/test-user.ts @@ -1,10 +1,16 @@ import chai from "chai"; import chaiHttp from "chai-http"; +import * as nextAuth from "next-auth/next"; import nock from "nock"; +import proxyquire from "proxyquire"; import sinon from "sinon"; import testUsers from "./users.json"; import utils from "./utils"; +import { createEmail as createEmailAction } from "@/app/api/member/actions/createEmailForUser"; +import { createRedirectionForUser } from "@/app/api/member/actions/createRedirectionForUser"; +import { deleteRedirectionForUser } from "@/app/api/member/actions/deleteRedirectionForUser"; +import { updatePasswordForUser } from "@/app/api/member/actions/updatePasswordForUser"; import { db } from "@/lib/kysely"; import * as mattermost from "@/lib/mattermost"; import { Domaine, EmailStatusCode } from "@/models/member"; @@ -17,7 +23,6 @@ import app from "@/server/index"; import betagouv from "@betagouv"; import Betagouv from "@betagouv"; import * as controllerUtils from "@controllers/utils"; -import knex from "@db"; import { createEmailAddresses, createRedirectionEmailAdresses, @@ -25,69 +30,120 @@ import { unsubscribeEmailAddresses, } from "@schedulers/emailScheduler"; +const deleteEmailForUser = proxyquire("@/app/api/member/actions", { + "next/cache": { + revalidatePath: sinon.stub(), + }, +}).deleteEmailForUser; + chai.use(chaiHttp); +const { expect } = chai; describe("User", () => { let ovhPasswordNock; - describe("POST /api/users/:username/create-email unauthenticated", () => { - it("should return an Unauthorized error", (done) => { - chai.request(app) - .post( - routes.USER_CREATE_EMAIL_API.replace( - ":username", - "membre.parti" - ) - ) - .type("form") - .send({ - _method: "POST", - }) - .end((err, res) => { - res.should.have.status(401); - done(); + describe("test createEmailAction unauthenticated", () => { + let getServerSessionStub; + let isPublicServiceEmailStub; + let user; + + beforeEach(async () => { + isPublicServiceEmailStub = sinon + .stub(controllerUtils, "isPublicServiceEmail") + .returns(Promise.resolve(true)); + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves({}); + + await utils.createUsers(testUsers); + user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.actif") + .executeTakeFirstOrThrow(); + }); + afterEach(async () => { + sinon.restore(); + await utils.deleteUsers(testUsers); + isPublicServiceEmailStub.restore(); + }); + + it("should return an Unauthorized error", async () => { + try { + await createEmailAction({ + username: "membre.parti", + to_email: "lucas.charr@test.com", }); + } catch (err) { + expect(err).to.be.an("error"); + } + // chai.request(app) + // .post( + // routes.USER_CREATE_EMAIL_API.replace( + // ":username", + // "membre.parti" + // ) + // ) + // .type("form") + // .send({ + // _method: "POST", + // }) + // .end((err, res) => { + // res.should.have.status(401); + // done(); + // }); }); }); - describe("POST /api/users/:username/create-email authenticated", () => { - let getToken; - let sendEmailStub; + describe("test createEmailAction authenticated", () => { + let getServerSessionStub; + let isPublicServiceEmailStub; + let user; + beforeEach(async () => { - sendEmailStub = sinon - .stub(controllerUtils, "sendMail") + isPublicServiceEmailStub = sinon + .stub(controllerUtils, "isPublicServiceEmail") .returns(Promise.resolve(true)); - getToken = sinon.stub(session, "getToken"); - getToken.returns(utils.getJWT("membre.actif")); + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves({}); + await utils.createUsers(testUsers); + user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.actif") + .executeTakeFirstOrThrow(); }); - afterEach(async () => { - sendEmailStub.restore(); - getToken.restore(); + sinon.restore(); await utils.deleteUsers(testUsers); + isPublicServiceEmailStub.restore(); }); it("should ask OVH to create an email", async () => { + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); const ovhEmailCreation = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account/) .reply(200); await db .updateTable("users") - .where("username", "=", "membre.nouveau") + .where("username", "=", "membre.nouveau@beta.gouv.fr") .set({ primary_email: null, }) .execute(); - await chai - .request(app) - .post( - routes.USER_CREATE_EMAIL_API.replace( - ":username", - "membre.nouveau" - ) - ) - .type("form") - .send({}); + + try { + await createEmailAction({ + username: "membre.nouveau", + to_email: "membre.nouveau@beta.gouv.fr", + }); + } catch (err) { + expect(err).to.be.an("error"); + } const res = await db .selectFrom("users") @@ -98,7 +154,7 @@ describe("User", () => { ovhEmailCreation.isDone().should.be.true; }); - it("should not allow email creation from delegate if email already exists", (done) => { + it("should not allow email creation from delegate if email already exists", async () => { // For this case we need to reset the basic nocks in order to return // a different response to indicate that membre.nouveau has an // existing email already created. @@ -109,6 +165,11 @@ describe("User", () => { utils.mockOvhTime(); utils.mockOvhRedirections(); + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); + // We return an email for membre.nouveau to indicate he already has one nock(/.*ovh.com/) .get(/^.*email\/domain\/.*\/account\/.*/) @@ -120,84 +181,82 @@ describe("User", () => { const ovhEmailCreation = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account/) .reply(200); - - chai.request(app) - .post( - routes.USER_CREATE_EMAIL_API.replace( - ":username", - "membre.nouveau" - ) - ) - .type("form") - .send({}) - .end((err, res) => { - ovhEmailCreation.isDone().should.be.false; - done(); + try { + await createEmailAction({ + username: "membre.nouveau", + to_email: "membre.nouveau@example.com", }); + } catch (err) { + ovhEmailCreation.isDone().should.be.false; + } }); - it("should not allow email creation from delegate if github file doesn't exist", (done) => { + it("should not allow email creation from delegate if github file doesn't exist", async () => { + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); const ovhEmailCreation = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account/) .reply(200); - - chai.request(app) - .post( - routes.USER_CREATE_EMAIL_API.replace( - ":username", - "membre.sans.fiche" - ) - ) - .type("form") - .send({}) - .end((err, res) => { - ovhEmailCreation.isDone().should.be.false; - done(); + try { + await createEmailAction({ + username: "membre.sans.fiche", + to_email: "membre.nouveau@example.com", }); + } catch (err) { + ovhEmailCreation.isDone().should.be.false; + } }); - it("should not allow email creation from delegate if user has expired", (done) => { + it("should not allow email creation from delegate if user has expired", async () => { + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); const ovhEmailCreation = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account/) .reply(200); - chai.request(app) - .post( - routes.USER_CREATE_EMAIL_API.replace( - ":username", - "membre.expire" - ) - ) - .type("form") - .send({}) - .end((err, res) => { - ovhEmailCreation.isDone().should.be.false; - done(); + try { + await createEmailAction({ + username: "membre.expire", + to_email: "membre.nouveau@example.com", }); + } catch (err) { + ovhEmailCreation.isDone().should.be.false; + } }); - it("should not allow email creation from delegate if delegate has expired", (done) => { + it("should not allow email creation from delegate if delegate has expired", async () => { + const mockSession = { + user: { id: "membre.expire", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); const ovhEmailCreation = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account/) .reply(200); - getToken.returns(utils.getJWT("membre.expire")); - chai.request(app) - .post( - routes.USER_CREATE_EMAIL_API.replace( - ":username", - "membre.nouveau" - ) - ) - .type("form") - .send({}) - .end((err, res) => { - ovhEmailCreation.isDone().should.be.false; - done(); + try { + await createEmailAction({ + username: "membre.nouveau", + to_email: "membre.nouveau@example.com", }); + } catch (err) { + ovhEmailCreation.isDone().should.be.false; + } }); it("should allow email creation from delegate if user is active", async () => { + const mockSession = { + user: { + id: "julien.dauphant", + isAdmin: false, + uuid: user.uuid, + }, + }; + getServerSessionStub.resolves(mockSession); + const ovhEmailCreation = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account/) .reply(200); @@ -208,19 +267,13 @@ describe("User", () => { primary_email: null, }) .execute(); - getToken.returns(utils.getJWT("julien.dauphant")); - await chai - .request(app) - .post( - routes.USER_CREATE_EMAIL_API.replace( - ":username", - "membre.actif" - ) - ) - .type("form") - .send({}); + await createEmailAction({ + username: "membre.actif", + to_email: "membre.nouveau@example.com", + }); + ovhEmailCreation.isDone().should.be.true; - const user = await db + const user2 = await db .selectFrom("users") .selectAll() .where("username", "=", "membre.actif") @@ -228,232 +281,216 @@ describe("User", () => { }); }); - describe("POST /api/users/:username/create-email unauthenticated", () => { - it("should return an Unauthorized error", (done) => { - chai.request(app) - .post("/api/users/membre.parti/create-email") - .send({ - _method: "POST", - to_email: "test@example.com", - }) - .end((err, res) => { - res.should.have.status(401); - done(); - }); - }); - }); - describe("POST /api/users/:username/create-email authenticated", () => { - let getToken; - let sendEmailStub; - beforeEach(async () => { - getToken = sinon.stub(session, "getToken"); - getToken.returns(utils.getJWT("membre.actif")); - sendEmailStub = sinon - .stub(controllerUtils, "sendMail") - .returns(Promise.resolve(true)); - await utils.createUsers(testUsers); - }); - - afterEach(async () => { - sendEmailStub.restore(); - getToken.restore(); - await utils.deleteUsers(testUsers); - }); - - it("should ask OVH to create an email", async () => { - const ovhEmailCreation = nock(/.*ovh.com/) - .post(/^.*email\/domain\/.*\/account/) - .reply(200); - await db - .updateTable("users") - .where("username", "=", "membre.nouveau") - .set({ - primary_email: null, - }) - .execute(); - await chai - .request(app) - .post("/api/users/membre.nouveau/create-email") - .send({ - to_email: "test@example.com", - }); - - const res = await db - .selectFrom("users") - .selectAll() - .where("username", "=", "membre.nouveau") - .executeTakeFirstOrThrow(); - res.primary_email.should.equal(`membre.nouveau@${config.domain}`); - ovhEmailCreation.isDone().should.be.true; - }); - }); - - describe("POST /api/users/:username/redirections unauthenticated", () => { - it("should return an Unauthorized error", (done) => { - chai.request(app) - .post("/api/users/membre.parti/redirections") - .type("form") - .send({ - to_email: "test@example.com", - }) - .end((err, res) => { - res.should.have.status(401); - done(); + describe("Create redirection unauthenticated", () => { + it("should return an Unauthorized error", async () => { + try { + await createRedirectionForUser({ + username: "membre.actif", + to_email: "toto@gmail.com", }); + } catch (err) { + expect(err).to.be.an("error"); + } }); }); - describe("POST /api/users/:username/redirections authenticated", () => { - let getToken; + describe("Create redirection authenticated", () => { + let getServerSessionStub; let isPublicServiceEmailStub; + let user; beforeEach(async () => { - getToken = sinon.stub(session, "getToken"); - getToken.returns(utils.getJWT("membre.actif")); isPublicServiceEmailStub = sinon .stub(controllerUtils, "isPublicServiceEmail") .returns(Promise.resolve(true)); + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves({}); + await utils.createUsers(testUsers); + user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.actif") + .executeTakeFirstOrThrow(); }); - afterEach(async () => { - getToken.restore(); + sinon.restore(); + await utils.deleteUsers(testUsers); isPublicServiceEmailStub.restore(); await utils.deleteUsers(testUsers); }); - it("should ask OVH to create a redirection", (done) => { + it("should ask OVH to create a redirection", async () => { + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); + isPublicServiceEmailStub.returns(Promise.resolve(true)); const ovhRedirectionCreation = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/redirection/) .reply(200); - chai.request(app) - .post("/api/users/membre.actif/redirections") - .type("form") - .send({ - to_email: "test@example.com", - }) - .end((err, res) => { - ovhRedirectionCreation.isDone().should.be.true; - done(); - }); + await createRedirectionForUser({ + to_email: "test@example.com", + username: "membre.actif", + }); + + ovhRedirectionCreation.isDone().should.be.true; }); - it("should not allow redirection creation from delegate", (done) => { + it("should not allow redirection creation from delegate", async () => { + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); const ovhRedirectionCreation = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/redirection/) .reply(200); - chai.request(app) - .post("/api/users/membre.nouveau/redirections") - .type("form") - .send({ + try { + await createRedirectionForUser({ to_email: "test@example.com", - }) - .end((err, res) => { - ovhRedirectionCreation.isDone().should.be.false; - done(); + username: "membre.nouveau", }); + } catch (e) { + ovhRedirectionCreation.isDone().should.be.false; + } }); - it("should not allow redirection creation from expired users", (done) => { + it("should not allow redirection creation from expired users", async () => { const ovhRedirectionCreation = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/redirection/) .reply(200); - getToken.returns(utils.getJWT("membre.expire")); - chai.request(app) - .post("/api/users/membre.expire/redirections") - .type("form") - .send({ + user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.expire") + .executeTakeFirstOrThrow(); + const mockSession = { + user: { id: "membre.expire", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); + try { + await createRedirectionForUser({ to_email: "test@example.com", - }) - .end((err, res) => { - ovhRedirectionCreation.isDone().should.be.false; - done(); + username: "membre.expire", }); + } catch (e) { + ovhRedirectionCreation.isDone().should.be.false; + } }); }); - describe("Delete /api/users/:username/redirections/:email/delete unauthenticated", () => { - it("should return an Unauthorized error", (done) => { - chai.request(app) - .delete( - "/api/users/membre.parti/redirections/test@example.com/delete" - ) - .end((err, res) => { - res.should.have.status(401); - done(); + describe("Delete redirections unauthenticated", () => { + let getServerSessionStub; + + beforeEach(async () => { + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves({}); + + await utils.createUsers(testUsers); + }); + afterEach(async () => { + sinon.restore(); + await utils.deleteUsers(testUsers); + }); + it("should return an Unauthorized error", async () => { + try { + await deleteRedirectionForUser({ + username: "membre.parti", + toEmail: "", }); + } catch (e) { + console.log(e); + } }); }); - describe("Delete /api/users/:username/redirections/:email/delete authenticated", () => { - let getToken; + describe("Delete redirections authenticated", () => { + let getServerSessionStub; let isPublicServiceEmailStub; + let user; beforeEach(async () => { - getToken = sinon.stub(session, "getToken"); - getToken.returns(utils.getJWT("membre.actif")); isPublicServiceEmailStub = sinon .stub(controllerUtils, "isPublicServiceEmail") .returns(Promise.resolve(true)); + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves({}); + await utils.createUsers(testUsers); + user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.actif") + .executeTakeFirstOrThrow(); }); - afterEach(async () => { - getToken.restore(); - isPublicServiceEmailStub.restore(); + sinon.restore(); await utils.deleteUsers(testUsers); + isPublicServiceEmailStub.restore(); }); it("should ask OVH to delete a redirection", async () => { + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); const ovhRedirectionDeletion = nock(/.*ovh.com/) .delete(/^.*email\/domain\/.*\/redirection\/.*/) .reply(200); - const res = await chai - .request(app) - .delete( - "/api/users/membre.actif/redirections/test-2@example.com/delete" - ); + await deleteRedirectionForUser({ + username: "membre.actif", + toEmail: "test-2@example.com", + }); ovhRedirectionDeletion.isDone().should.be.true; }); - it("should not allow redirection deletion from delegate", (done) => { + it("should not allow redirection deletion from delegate", async () => { + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); const ovhRedirectionDeletion = nock(/.*ovh.com/) .delete(/^.*email\/domain\/.*\/redirection\/.*/) .reply(200); - - chai.request(app) - .delete( - "/api/users/membre.nouveau/redirections/test-2@example.com/delete" - ) - .end((err, res) => { - ovhRedirectionDeletion.isDone().should.be.false; - done(); + try { + await deleteRedirectionForUser({ + username: "membre.nouveau", + toEmail: "test-2@example.com", }); + } catch (e) { + ovhRedirectionDeletion.isDone().should.be.false; + } }); - it("should not allow redirection deletion from expired users", (done) => { + it("should not allow redirection deletion from expired users", async () => { const ovhRedirectionDeletion = nock(/.*ovh.com/) .delete(/^.*email\/domain\/.*\/redirection\/.*/) .reply(200); - getToken.returns(utils.getJWT("membre.expire")); + const mockSession = { + user: { id: "membre.expire", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); - chai.request(app) - .delete( - "/api/users/membre.expire/redirections/test-2@example.com/delete" - ) - .end((err, res) => { - ovhRedirectionDeletion.isDone().should.be.false; - done(); + try { + await deleteRedirectionForUser({ + username: "membre.expire", + toEmail: "test-2@example.com", }); + } catch (e) { + ovhRedirectionDeletion.isDone().should.be.false; + } }); }); - describe("POST /users/:username/password unauthenticated", () => { + describe("Test update password server action unauthenticated", () => { beforeEach(async () => { await utils.createUsers(testUsers); }); @@ -463,65 +500,72 @@ describe("User", () => { await utils.deleteUsers(testUsers); }); - it("should return an Unauthorized error", (done) => { - chai.request(app) - .post("/api/users/membre.actif/password") - .type("form") - .send({ + it("should return an Unauthorized error", async () => { + try { + await updatePasswordForUser({ + username: "membre.actif", new_password: "Test_Password_1234", - }) - .end((err, res) => { - res.should.have.status(401); - done(); }); + } catch (e) { + expect(e).to.be.an("error"); + } }); - it("should not allow a password change", (done) => { + it("should not allow a password change", async () => { ovhPasswordNock = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account\/.*\/changePassword/) .reply(200); - chai.request(app) - .post("/api/users/membre.actif/password") - .type("form") - .send({ + try { + await updatePasswordForUser({ new_password: "Test_Password_1234", - }) - .end((err, res) => { - ovhPasswordNock.isDone().should.be.false; - done(); + username: "membre.actif", }); + } catch (e) { + ovhPasswordNock.isDone().should.be.false; + } }); }); - describe("POST /api/users/:username/password authenticated", () => { - let getToken; + describe("Test update password server action authenticated", () => { + let getServerSessionStub; let isPublicServiceEmailStub; + let user; + beforeEach(async () => { - getToken = sinon.stub(session, "getToken"); - getToken.returns(utils.getJWT("membre.actif")); isPublicServiceEmailStub = sinon .stub(controllerUtils, "isPublicServiceEmail") .returns(Promise.resolve(true)); + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves({}); + await utils.createUsers(testUsers); + user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.actif") + .executeTakeFirstOrThrow(); }); - afterEach(async () => { - getToken.restore(); + sinon.restore(); + await utils.deleteUsers(testUsers); isPublicServiceEmailStub.restore(); await utils.deleteUsers(testUsers); }); it("should send error if user does not exist", async () => { - const res = await chai - .request(app) - .post("/api/users/membre.actif/password") - .type("form") - .send({ + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); + try { + await updatePasswordForUser({ new_password: "Test_Password_1234", - }) - .redirects(0); - // .end((err, res) => { - res.should.have.status(500); + username: "membre.onthetom", + }); + } catch (e) { + expect(e).to.be.an("error"); + } // res.header.location.should.equal("/community/membre.actif"); // done(); // }); @@ -534,7 +578,11 @@ describe("User", () => { utils.mockSlackSecretariat(); utils.mockOvhTime(); utils.mockOvhRedirections(); - const username = "membre.nouveau"; + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); + const username = "membre.actif"; await db .updateTable("users") .where("username", "=", username) @@ -544,21 +592,21 @@ describe("User", () => { .get(/^.*email\/domain\/.*\/account\/.*/) .reply(200, { accountName: username, - email: "membre.nouveau@example.com", + email: "membre.actif@example.com", }) .persist(); ovhPasswordNock = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account\/.*\/changePassword/) .reply(200); - getToken.returns(utils.getJWT(`${username}`)); - await chai - .request(app) - .post(`/api/users/${username}/password`) - .type("form") - .send({ + try { + await updatePasswordForUser({ + username, new_password: "Test_Password_1234", }); + } catch (e) { + expect(e).to.be.an("error"); + } ovhPasswordNock.isDone().should.be.true; }); it("should perform a password change and pass status to active if status was suspended", async () => { @@ -569,7 +617,11 @@ describe("User", () => { utils.mockSlackSecretariat(); utils.mockOvhTime(); utils.mockOvhRedirections(); - const username = "membre.nouveau"; + const username = "membre.actif"; + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); await db .updateTable("users") .where("username", "=", username) @@ -581,117 +633,131 @@ describe("User", () => { .get(/^.*email\/domain\/.*\/account\/.*/) .reply(200, { accountName: username, - email: "membre.nouveau@example.com", + email: "membre.actif@example.com", }) .persist(); ovhPasswordNock = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account\/.*\/changePassword/) .reply(200); - getToken.returns(utils.getJWT(`${username}`)); - await chai - .request(app) - .post(`/api/users/${username}/password`) - .type("form") - .send({ - new_password: "Test_Password_1234", - }); + await updatePasswordForUser({ + new_password: "Test_Password_1234", + username: username, + }); + ovhPasswordNock.isDone().should.be.true; - const user = await db + const user2 = await db .selectFrom("users") .selectAll() .where("username", "=", username) - .executeTakeFirst(); - user.primary_email_status.should.be.equal( + .executeTakeFirstOrThrow(); + user2.primary_email_status.should.be.equal( EmailStatusCode.EMAIL_ACTIVE ); }); - it("should not allow a password change from delegate", (done) => { + it("should not allow a password change from delegate", async () => { + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); + ovhPasswordNock = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account\/.*\/changePassword/) .reply(200); - - chai.request(app) - .post("/api/users/membre.nouveau/password") - .type("form") - .send({ + try { + await updatePasswordForUser({ new_password: "Test_Password_1234", - }) - .end((err, res) => { - ovhPasswordNock.isDone().should.be.false; - done(); + username: "membre.nouveau", }); + } catch (e) { + expect(e).to.be.an("error"); + } }); - it("should not allow a password change from expired user", (done) => { + it("should not allow a password change from expired user", async () => { ovhPasswordNock = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account\/.*\/changePassword/) .reply(200); - getToken.returns(utils.getJWT("membre.expire")); + user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.expire") + .executeTakeFirstOrThrow(); + const mockSession = { + user: { id: "membre.expire", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); - chai.request(app) - .post("/api/users/membre.expire/password") - .type("form") - .send({ + try { + await updatePasswordForUser({ new_password: "Test_Password_1234", - }) - .end((err, res) => { - ovhPasswordNock.isDone().should.be.false; - done(); + username: "membre.expire", }); + } catch (e) { + ovhPasswordNock.isDone().should.be.false; + } }); - it("should not allow a password shorter than 9 characters", (done) => { + it("should not allow a password shorter than 9 characters", async () => { ovhPasswordNock = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account\/.*\/changePassword/) .reply(200); - - chai.request(app) - .post("/api/users/membre.actif/password") - .type("form") - .send({ + const mockSession = { + user: { id: "membre.actif", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); + try { + await updatePasswordForUser({ new_password: "12345678", - }) - .end((err, res) => { - ovhPasswordNock.isDone().should.be.false; - done(); + username: "membre.actif", }); + } catch (e) { + ovhPasswordNock.isDone().should.be.false; + } }); - it("should not allow a password longer than 30 characters", (done) => { + it("should not allow a password longer than 30 characters", async () => { ovhPasswordNock = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/account\/.*\/changePassword/) .reply(200); - chai.request(app) - .post("/api/users/membre.actif/password") - .type("form") - .send({ + try { + await updatePasswordForUser({ new_password: "1234567890123456789012345678901", - }) - .end((err, res) => { - ovhPasswordNock.isDone().should.be.false; - done(); + username: "membre.actif", }); + } catch (e) { + ovhPasswordNock.isDone().should.be.false; + } }); }); - describe("POST /users/:username/email/delete unauthenticated", () => { - it("should return an Unauthorized error", (done) => { - chai.request(app) - .post("/api/users/membre.parti/email/delete") - .end((err, res) => { - res.should.have.status(401); - done(); + describe("Delete user email when unauthenticated", () => { + let getServerSessionStub; + beforeEach(() => { + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves(undefined); + }); + afterEach(() => { + getServerSessionStub.restore(); + }); + it("should return an Unauthorized error", async () => { + try { + await deleteEmailForUser({ + username: "membre.parti", }); + } catch (err) { + expect(err).to.be.an("error"); + } }); }); - describe("POST /user/:username/email/delete", () => { - let getToken; + describe("Delete user email", () => { let isPublicServiceEmailStub; - + let getServerSessionStub; beforeEach(async () => { - getToken = sinon.stub(session, "getToken"); - getToken.returns(utils.getJWT("membre.actif")); + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves({}); isPublicServiceEmailStub = sinon .stub(controllerUtils, "isPublicServiceEmail") .returns(Promise.resolve(true)); @@ -699,11 +765,24 @@ describe("User", () => { }); afterEach(async () => { - getToken.restore(); + getServerSessionStub.restore(); isPublicServiceEmailStub.restore(); await utils.deleteUsers(testUsers); }); it("should keep the user in database secretariat", async () => { + const user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.actif") + .executeTakeFirstOrThrow(); + const mockSession = { + user: { + id: "membre.actif", + isAdmin: false, + uuid: user.uuid, + }, + }; + getServerSessionStub.resolves(mockSession); const addRedirection = nock(/.*ovh.com/) .post(/^.*email\/domain\/.*\/redirection/) .reply(200); @@ -714,9 +793,7 @@ describe("User", () => { .where("username", "=", "membre.actif") .execute(); dbRes.length.should.equal(1); - await chai - .request(app) - .post("/api/users/membre.actif/email/delete"); + await deleteEmailForUser({ username: "membre.actif" }); const dbNewRes = await db .selectFrom("users") .selectAll() @@ -726,7 +803,15 @@ describe("User", () => { addRedirection.isDone().should.be.true; }); - it("should ask OVH to redirect to the departs email", (done) => { + it("should ask OVH to redirect to the departs email", async () => { + const mockSession = { + user: { + id: "membre.actif", + isAdmin: false, + uuid: "membre.actif", + }, + }; + getServerSessionStub.resolves(mockSession); const expectedRedirectionBody = (body) => { return ( body.from === `membre.actif@${config.domain}` && @@ -740,46 +825,48 @@ describe("User", () => { expectedRedirectionBody ) .reply(200); - - chai.request(app) - .post("/api/users/membre.actif/email/delete") - .end((err, res) => { - ovhRedirectionDepartureEmail.isDone().should.be.true; - done(); - }); + await deleteEmailForUser({ username: "membre.actif" }); + ovhRedirectionDepartureEmail.isDone().should.be.true; }); }); - describe("POST /users/:username/secondary_email", () => { - let getToken; + describe("Test manage secondary email", () => { let isPublicServiceEmailStub; + let getServerSessionStub; + let user; + const manageSecondaryEmailForUser = proxyquire( + "@/app/api/member/actions", + { + "next/cache": { + revalidatePath: sinon.stub(), + }, + } + ).manageSecondaryEmailForUser; beforeEach(async () => { - getToken = sinon.stub(session, "getToken"); - getToken.returns(utils.getJWT("membre.nouveau")); - await utils.createUsers(testUsers); isPublicServiceEmailStub = sinon .stub(controllerUtils, "isPublicServiceEmail") .returns(Promise.resolve(true)); - }); + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves({}); + await utils.createUsers(testUsers); + user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.nouveau") + .executeTakeFirstOrThrow(); + const mockSession = { + user: { id: "membre.nouveau", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); + }); afterEach(async () => { - getToken.restore(); + sinon.restore(); await utils.deleteUsers(testUsers); isPublicServiceEmailStub.restore(); - }); - it("should return 200 to add secondary email", async () => { - const username = "membre.nouveau"; - const secondaryEmail = "membre.nouveau.perso@example.com"; - const res = await chai - .request(app) - .post("/api/users/membre.nouveau/secondary_email") - .type("form") - .send({ - username, - secondaryEmail, - }); - res.should.have.status(200); + await utils.deleteUsers(testUsers); }); it("should add secondary email", async () => { @@ -791,14 +878,11 @@ describe("User", () => { .selectAll() .where("username", "=", "membre.nouveau") .execute(); - await chai - .request(app) - .post(`/api/users/${username}/secondary_email`) - .type("form") - .send({ - username, - secondaryEmail, - }); + await manageSecondaryEmailForUser({ + username, + secondaryEmail, + }); + const dbNewRes = await db .selectFrom("users") .selectAll() @@ -820,14 +904,10 @@ describe("User", () => { secondary_email: secondaryEmail, }) .execute(); - await chai - .request(app) - .post(`/api/users/${username}/secondary_email/`) - .type("form") - .send({ - username, - secondaryEmail: newSecondaryEmail, - }); + await manageSecondaryEmailForUser({ + username, + secondaryEmail: newSecondaryEmail, + }); const dbNewRes = await db .selectFrom("users") .selectAll() @@ -845,10 +925,19 @@ describe("User", () => { }); }); - describe("PUT /api/users/:username/primary_email", () => { + describe("Test action managePrimaryEmailForUser", () => { let mattermostGetUserByEmailStub; let isPublicServiceEmailStub; - let getToken; + let getServerSessionStub; + let user; + const managePrimaryEmailForUser = proxyquire( + "@/app/api/member/actions", + { + "next/cache": { + revalidatePath: sinon.stub(), + }, + } + ).managePrimaryEmailForUser; beforeEach(async () => { mattermostGetUserByEmailStub = sinon @@ -857,30 +946,38 @@ describe("User", () => { isPublicServiceEmailStub = sinon .stub(controllerUtils, "isPublicServiceEmail") .returns(Promise.resolve(true)); - getToken = sinon.stub(session, "getToken"); - getToken.returns(utils.getJWT("membre.nouveau")); + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves({}); + await utils.createUsers(testUsers); + user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.nouveau") + .executeTakeFirstOrThrow(); }); afterEach(async () => { + sinon.restore(); + await utils.deleteUsers(testUsers); mattermostGetUserByEmailStub.restore(); isPublicServiceEmailStub.restore(); - getToken.restore(); await utils.deleteUsers(testUsers); }); it("should not update primary email if user is not current user", async () => { + const mockSession = { + user: { id: "anyuser", isAdmin: false, uuid: "anyuser-uuid" }, + }; + getServerSessionStub.resolves(mockSession); const username = "membre.nouveau"; const primaryEmail = "membre.nouveau.new@example.com"; - getToken.returns(utils.getJWT("julien.dauphant")); + try { + await managePrimaryEmailForUser({ username, primaryEmail }); + } catch (e) { + console.log(e); + } - await chai - .request(app) - .put(`/api/users/${username}/primary_email/`) - .type("form") - .send({ - username, - primaryEmail: primaryEmail, - }); isPublicServiceEmailStub.called.should.be.true; mattermostGetUserByEmailStub.calledTwice.should.be.false; }); @@ -889,23 +986,29 @@ describe("User", () => { const username = "membre.nouveau"; const primaryEmail = "membre.nouveau.new@example.com"; isPublicServiceEmailStub.returns(Promise.resolve(false)); - getToken.returns(utils.getJWT("membre.nouveau")); - await chai - .request(app) - .put(`/api/users/${username}/primary_email/`) - .type("form") - .send({ - username, - primaryEmail: primaryEmail, - }); + const user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.nouveau") + .executeTakeFirstOrThrow(); + + const mockSession = { + user: { id: "membre.nouveau", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); + + try { + await managePrimaryEmailForUser({ username, primaryEmail }); + } catch (e) { + console.log(e); + } const dbNewRes = await db .selectFrom("users") .selectAll() .where("username", "=", "membre.nouveau") - .execute(); - dbNewRes.length.should.equal(1); - dbNewRes[0].primary_email.should.not.equal(primaryEmail); + .executeTakeFirstOrThrow(); + dbNewRes.primary_email?.should.not.equal(primaryEmail); isPublicServiceEmailStub.called.should.be.true; mattermostGetUserByEmailStub.calledOnce.should.be.false; }); @@ -915,7 +1018,11 @@ describe("User", () => { mattermostGetUserByEmailStub.returns(Promise.reject("404 error")); const username = "membre.nouveau"; const primaryEmail = "membre.nouveau.new@example.com"; - getToken.returns(utils.getJWT("membre.nouveau")); + const mockSession = { + user: { id: "membre.nouveau", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); + await db .updateTable("users") .where("username", "=", "membre.nouveau") @@ -924,21 +1031,17 @@ describe("User", () => { }) .execute(); - const res = await chai - .request(app) - .put(`/api/users/${username}/primary_email/`) - .type("form") - .send({ - username, - primaryEmail: primaryEmail, - }); + try { + await managePrimaryEmailForUser({ username, primaryEmail }); + } catch (e) { + console.log(e); + } const dbNewRes = await db .selectFrom("users") .selectAll() .where("username", "=", "membre.nouveau") - .execute(); - dbNewRes.length.should.equal(1); - dbNewRes[0].primary_email.should.not.equal(primaryEmail); + .executeTakeFirstOrThrow(); + dbNewRes.primary_email?.should.not.equal(primaryEmail); mattermostGetUserByEmailStub.calledOnce.should.be.true; @@ -956,7 +1059,11 @@ describe("User", () => { mattermostGetUserByEmailStub.returns(Promise.reject("404 error")); const username = "membre.nouveau"; const primaryEmail = "admin@otherdomaine.gouv.fr"; - getToken.returns(utils.getJWT("membre.nouveau")); + const mockSession = { + user: { id: "membre.nouveau", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); + await db .updateTable("users") .where("username", "=", "membre.nouveau") @@ -965,21 +1072,15 @@ describe("User", () => { }) .execute(); - await chai - .request(app) - .put(`/api/users/${username}/primary_email/`) - .type("form") - .send({ - username, - primaryEmail: primaryEmail, - }); + try { + await managePrimaryEmailForUser({ username, primaryEmail }); + } catch (e) {} const dbNewRes = await db .selectFrom("users") .selectAll() .where("username", "=", "membre.nouveau") - .execute(); - dbNewRes.length.should.equal(1); - dbNewRes[0].primary_email.should.equal( + .executeTakeFirstOrThrow(); + dbNewRes.primary_email?.should.equal( `membre.nouveau@${config.domain}` ); await db @@ -1002,23 +1103,19 @@ describe("User", () => { .returns(Promise.resolve(true)); const username = "membre.nouveau"; const primaryEmail = "membre.nouveau.new@example.com"; - getToken.returns(utils.getJWT("membre.nouveau")); + const mockSession = { + user: { id: "membre.nouveau", isAdmin: false, uuid: user.uuid }, + }; + getServerSessionStub.resolves(mockSession); + + await managePrimaryEmailForUser({ username, primaryEmail }); - const res = await chai - .request(app) - .put(`/api/users/${username}/primary_email/`) - .type("form") - .send({ - username, - primaryEmail: primaryEmail, - }); const dbNewRes = await db .selectFrom("users") .selectAll() .where("username", "=", "membre.nouveau") - .execute(); - dbNewRes.length.should.equal(1); - dbNewRes[0].primary_email.should.equal(primaryEmail); + .executeTakeFirstOrThrow(); + dbNewRes.primary_email?.should.equal(primaryEmail); await db .updateTable("users") .where("username", "=", "membre.nouveau") @@ -1035,27 +1132,41 @@ describe("User", () => { }); }); - describe("Post delete /api/users/:username/email/delete authenticated", () => { - let getToken; + describe("Email delete", () => { let isPublicServiceEmailStub; + let getServerSessionStub; beforeEach(async () => { - getToken = sinon.stub(session, "getToken"); - getToken.returns(utils.getJWT("membre.actif")); - await utils.createUsers(testUsers); + getServerSessionStub = sinon + .stub(nextAuth, "getServerSession") + .resolves({}); isPublicServiceEmailStub = sinon .stub(controllerUtils, "isPublicServiceEmail") .returns(Promise.resolve(true)); + await utils.createUsers(testUsers); }); afterEach(async () => { - getToken.restore(); - await utils.deleteUsers(testUsers); + getServerSessionStub.restore(); isPublicServiceEmailStub.restore(); + await utils.deleteUsers(testUsers); }); - it("Deleting email should ask OVH to delete all redirections", (done) => { + it("Deleting email should ask OVH to delete all redirections", async () => { nock.cleanAll(); + const user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.expire") + .executeTakeFirstOrThrow(); + const mockSession = { + user: { + id: "membre.expire", + isAdmin: false, + uuid: user.uuid, + }, + }; + getServerSessionStub.resolves(mockSession); nock(/.*ovh.com/) .get(/^.*email\/domain\/.*\/redirection/) @@ -1084,28 +1195,48 @@ describe("User", () => { .delete(/^.*email\/domain\/.*\/redirection\/123123/) .reply(200); - chai.request(app) - .post("/api/users/membre.expire/email/delete") - .end((err, res) => { - ovhRedirectionDeletion.isDone().should.be.true; - done(); - }); + await deleteEmailForUser({ username: "membre.expire" }); + ovhRedirectionDeletion.isDone().should.be.true; }); - it("should not allow email deletion for active users", (done) => { + it("should not allow email deletion for active users", async () => { const ovhEmailDeletion = nock(/.*ovh.com/) .delete(/^.*email\/domain\/.*\/account\/membre.expire/) .reply(200); - - chai.request(app) - .post("/api/users/membre.actif/email/delete") - .end((err, res) => { - ovhEmailDeletion.isDone().should.be.false; - done(); - }); + const user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.expire") + .executeTakeFirstOrThrow(); + const mockSession = { + user: { + id: "membre.actif", + isAdmin: false, + uuid: user.uuid, + }, + }; + getServerSessionStub.resolves(mockSession); + try { + await deleteEmailForUser({ username: "membre.actif" }); + } catch (e) { + ovhEmailDeletion.isDone().should.be.false; + } }); - it("should not allow email deletion for another user if active", (done) => { + it("should not allow email deletion for another user if active", async () => { + const user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.nouveau") + .executeTakeFirstOrThrow(); + const mockSession = { + user: { + id: "membre.nouveau", + isAdmin: false, + uuid: user.uuid, + }, + }; + getServerSessionStub.resolves(mockSession); nock.cleanAll(); const ovhEmailDeletion = nock(/.*ovh.com/) @@ -1120,17 +1251,28 @@ describe("User", () => { utils.mockSlackGeneral(); utils.mockSlackSecretariat(); - getToken.returns(utils.getJWT("membre.nouveau")); - chai.request(app) - .post("/api/users/membre.actif/email/delete") - .end((err, res) => { - ovhEmailDeletion.isDone().should.be.false; - done(); - }); + try { + await deleteEmailForUser({ username: "membre.actif" }); + } catch (e) { + ovhEmailDeletion.isDone().should.be.false; + } }); - it("should allow email deletion for requester even if active", (done) => { + it("should allow email deletion for requester even if active", async () => { nock.cleanAll(); + const user = await db + .selectFrom("users") + .selectAll() + .where("username", "=", "membre.actif") + .executeTakeFirstOrThrow(); + const mockSession = { + user: { + id: "membre.actif", + isAdmin: false, + uuid: user.uuid, + }, + }; + getServerSessionStub.resolves(mockSession); nock(/.*ovh.com/) .get(/^.*email\/domain\/.*\/redirection/) @@ -1163,12 +1305,11 @@ describe("User", () => { .delete(/^.*email\/domain\/.*\/redirection\/123123/) .reply(200); - chai.request(app) - .post("/api/users/membre.actif/email/delete") - .end((err, res) => { - ovhRedirectionDeletion.isDone().should.be.true; - done(); - }); + try { + await deleteEmailForUser({ username: "membre.actif" }); + } catch (e) { + ovhRedirectionDeletion.isDone().should.be.true; + } }); }); diff --git a/package.json b/package.json index 002388918..3bbe7396e 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,11 @@ "node": "20.10.0" }, "scripts": { - "dev": "node --max_old_space_size=8192 -r ts-node/register src/server/server.ts", - "build:next": "next build", - "build:server": "ttsc -p tsconfig.server.json", + "dev": "next dev -p 8100", "build": "npm run build:next && npm run build:server", - "start": "NODE_ENV=production node dist/src/server/server.js", + "build:server": "ttsc -p tsconfig.server.json", + "build:next": "next build", + "start": "NODE_ENV=production next start --port ${PORT-3000}", "lint": "next lint", "migrate": "knex migrate:latest --esm", "rolldown": "knex migrate:down --esm", diff --git a/src/app/(private)/(dashboard)/admin/mattermost/AdminMattermostClientPage.tsx b/src/app/(private)/(dashboard)/admin/mattermost/AdminMattermostClientPage.tsx index 8297b3aba..89c8c32de 100644 --- a/src/app/(private)/(dashboard)/admin/mattermost/AdminMattermostClientPage.tsx +++ b/src/app/(private)/(dashboard)/admin/mattermost/AdminMattermostClientPage.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import axios from "axios"; import type { Metadata } from "next"; +import { safeGetMattermostInfo } from "@/app/api/admin/actions/getMattermostAdmin"; import { AdminMattermost, AdminMattermostProps, @@ -12,16 +13,13 @@ import routes, { computeRoute } from "@/routes/routes"; export default function Page() { const [data, setData] = useState({}); - const [isLoading, setLoading] = useState(true); + const [isLoading] = useState(true); useEffect(() => { - axios - .get(computeRoute(routes.ADMIN_MATTERMOST_API), { - withCredentials: true, - }) - .then((res) => { - setData(res.data); - setLoading(false); - }); + async function fetchData() { + const res = await safeGetMattermostInfo(); + setData(res.data || {}); + } + fetchData(); }, []); if (isLoading) return

Chargement...

; diff --git a/src/app/api/admin/actions/getMattermostAdmin.ts b/src/app/api/admin/actions/getMattermostAdmin.ts new file mode 100644 index 000000000..24b568f36 --- /dev/null +++ b/src/app/api/admin/actions/getMattermostAdmin.ts @@ -0,0 +1,60 @@ +"use server"; + +import { getServerSession } from "next-auth/next"; + +import { MattermostChannel } from "@/lib/mattermost"; +import config from "@/server/config"; +import { authOptions } from "@/utils/authoptions"; +import { + AuthorizationError, + UnwrapPromise, + withErrorHandling, +} from "@/utils/error"; +import { getAllChannels } from "@infra/chat"; +import { + MattermostUserWithStatus, + getMattermostUsersWithStatus, +} from "@schedulers/mattermostScheduler/removeBetaAndParnersUsersFromCommunityTeam"; + +export async function getMattermostInfo() { + const session = await getServerSession(authOptions); + if (!session || !session.user.id) { + throw new AuthorizationError(); + } + if (!session.user.isAdmin) { + throw new AuthorizationError(`L'utilisateur doit être administrateur`); + } + + let users: MattermostUserWithStatus[] = []; + + if (process.env.NODE_ENV === "production") { + users = await getMattermostUsersWithStatus({ + nbDays: 90, + }); + } + + const channels: MattermostChannel[] = await getAllChannels( + config.mattermostTeamId + ); + try { + const title = "Admin Mattermost"; + return { + title, + users, + channelOptions: channels.map((channel) => ({ + value: channel.name, + label: channel.display_name, + })), + currentUserId: session.user.id, + isAdmin: config.ESPACE_MEMBRE_ADMIN.includes(session.user.id), + }; + } catch (err) { + console.error(err); + throw err; + } +} + +export const safeGetMattermostInfo = withErrorHandling< + UnwrapPromise>, + Parameters +>(getMattermostInfo); diff --git a/src/app/api/admin/actions/getMattermostUsersInfo.ts b/src/app/api/admin/actions/getMattermostUsersInfo.ts new file mode 100644 index 000000000..a3788bb11 --- /dev/null +++ b/src/app/api/admin/actions/getMattermostUsersInfo.ts @@ -0,0 +1,44 @@ +"use server"; + +import { getServerSession } from "next-auth/next"; + +import { MattermostUser } from "@/models/mattermost"; +import { getMattermostUsers } from "@/server/controllers/adminController"; +import { authOptions } from "@/utils/authoptions"; +import { + AuthorizationError, + UnwrapPromise, + withErrorHandling, +} from "@/utils/error"; + +export const getMattermostUsersInfo = async ({ + fromBeta, + excludeEmails, + includeEmails, +}: { + fromBeta: boolean; + excludeEmails?: string[]; + includeEmails?: string[]; +}) => { + const session = await getServerSession(authOptions); + if (!session || !session.user.id) { + throw new AuthorizationError(); + } + if (!session.user.isAdmin) { + throw new AuthorizationError(`L'utilisateur doit être administrateur`); + } + + const users: MattermostUser[] = await getMattermostUsers({ + fromBeta, + excludeEmails: excludeEmails || [], + includeEmails: includeEmails || [], + }); + return { + users, + }; +}; + +export const safeGetMattermostUsersInfo = withErrorHandling< + UnwrapPromise>, + Parameters +>(getMattermostUsersInfo); diff --git a/src/app/api/admin/action.ts b/src/app/api/admin/actions/index.ts similarity index 100% rename from src/app/api/admin/action.ts rename to src/app/api/admin/actions/index.ts diff --git a/src/app/api/admin/actions/sendMattermostMessage.ts b/src/app/api/admin/actions/sendMattermostMessage.ts new file mode 100644 index 000000000..c593ec266 --- /dev/null +++ b/src/app/api/admin/actions/sendMattermostMessage.ts @@ -0,0 +1,124 @@ +"use server"; + +import { getServerSession } from "next-auth/next"; + +import { getMattermostUsers } from "@/server/controllers/adminController"; +import { authOptions } from "@/utils/authoptions"; +import { + AuthorizationError, + UnwrapPromise, + withErrorHandling, +} from "@/utils/error"; +import { getUserWithParams, sendInfoToChat } from "@infra/chat"; + +const sendMessageToChannel = async ({ + channel, + text, +}: { + channel: string; + text: string; +}) => { + await sendInfoToChat({ + text: text, + channel, + }); +}; + +const sendDirectMessageToUsers = async ({ + fromBeta, + text, + excludeEmails, + includeEmails, +}: { + fromBeta: boolean; + text: string; + excludeEmails: string[]; + includeEmails: string[]; +}) => { + const activeUsers = await getMattermostUsers({ + fromBeta, + includeEmails, + excludeEmails, + }); + console.log(`Will send message to ${activeUsers.length}`); + let nbUsers = 0; + for (const user of activeUsers) { + console.log(`Will write to user`, user.username); + try { + await sendInfoToChat({ + text: text, + username: user.username, + channel: "secretariat", + extra: { + username: "Equipe Communauté beta.gouv", + }, + }); + nbUsers++; + } catch (e) {} + } + return { + nbUsers, + }; +}; + +export const sendMessageToUsersOnChat = async ({ + text, + fromBeta, + excludeEmails, + includeEmails, + channel, + prod, +}: { + text: string; + fromBeta: boolean; + excludeEmails?: string[]; + includeEmails?: string[]; + channel?: string; + prod?: boolean; +}) => { + const session = await getServerSession(authOptions); + if (!session || !session.user.id) { + throw new AuthorizationError( + `You don't have the right to access this function` + ); + } + if (!session.user.isAdmin) { + throw new AuthorizationError(`L'utilisateur doit être administrateur`); + } + let nbUsers; + if (prod) { + if (channel) { + await sendMessageToChannel({ + text, + channel, + }); + } else { + console.log("will send direct message to users"); + nbUsers = await sendDirectMessageToUsers({ + text, + fromBeta, + excludeEmails: excludeEmails || [], + includeEmails: includeEmails || [], + }).then((res) => res.nbUsers); + } + } + // send message to admin + await sendInfoToChat({ + text: text, + username: session.user.id, + channel: "secretariat", + extra: { + username: "Equipe Communauté beta.gouv", + }, + }); + return { + message: `Envoyé un message en ${prod ? "prod" : "test"} à ${ + nbUsers !== undefined ? nbUsers : channel + }`, + }; +}; + +export const safeSendMessageToUsersOnChat = withErrorHandling< + UnwrapPromise>, + Parameters +>(sendMessageToUsersOnChat); diff --git a/src/app/api/member/[username]/route.ts b/src/app/api/member/[username]/route.ts index 261c812b6..557a8b0e7 100644 --- a/src/app/api/member/[username]/route.ts +++ b/src/app/api/member/[username]/route.ts @@ -24,36 +24,6 @@ import { import { authOptions } from "@/utils/authoptions"; import { AdminEmailNotAllowedError } from "@/utils/error"; -const getMattermostUserInfo = async ( - dbUser -): Promise<{ - mattermostUser: MattermostUser | null; - mattermostUserInTeamAndActive: boolean; -}> => { - try { - let mattermostUser = dbUser?.primary_email - ? await getUserByEmail(dbUser.primary_email).catch((e) => null) - : null; - const [mattermostUserInTeamAndActive] = dbUser?.primary_email - ? await searchUsers({ - term: dbUser.primary_email, - team_id: config.mattermostTeamId, - allow_inactive: false, - }).catch((e) => []) - : []; - return { - mattermostUser, - mattermostUserInTeamAndActive, - }; - } catch (e) { - Sentry.captureException(e); - return { - mattermostUser: null, - mattermostUserInTeamAndActive: false, - }; - } -}; - export async function PUT( req: Request, { params: { username } }: { params: { username: string } } @@ -98,65 +68,3 @@ export async function PUT( data: dbUser, }); } - -// export async function GET( -// req: Request, -// { params: { username } }: { params: { username: string } } -// ) { -// const session = await getServerSession(authOptions); - -// if (!session || !session.user.id) { -// throw new Error(`You don't have the right to access this function`); -// } - -// const isCurrentUser = session.user.id === username; -// try { -// // todo not sure this call should send all user infos -// const user = await userInfos({ username }, isCurrentUser); -// const hasGithubFile = user.userInfos; -// const hasEmailAddress = -// user.emailInfos || user.emailRedirections.length > 0; -// if (!hasGithubFile && !hasEmailAddress) { -// throw new Error( -// 'Il n\'y a pas de membres avec ce compte mail. Vous pouvez créer la fiche de cette personne.' -// ); -// } - -// const dbUser = await getUserBasicInfo({ username }); -// const primaryEmail = dbUser ? dbUser.primary_email : ""; -// const secondaryEmail = dbUser ? dbUser.secondary_email : ""; -// let availableEmailPros: string[] = []; -// if (config.ESPACE_MEMBRE_ADMIN.includes(session.user.id)) { -// availableEmailPros = await betagouv.getAvailableProEmailInfos(); -// } -// let { mattermostUser, mattermostUserInTeamAndActive } = -// await getMattermostUserInfo(dbUser); -// const title = user.userInfos ? user.userInfos.fullname : null; - -// return Response.json({ -// userBaseInfos: dbUser, -// username, -// emailInfos: user.emailInfos, -// redirections: user.emailRedirections, -// userInfos: user.userInfos, -// isExpired: user.isExpired, -// isAdmin: config.ESPACE_MEMBRE_ADMIN.includes(session.user.id), -// availableEmailPros, -// mattermostInfo: { -// hasMattermostAccount: !!mattermostUser, -// isInactiveOrNotInTeam: !mattermostUserInTeamAndActive, -// }, -// primaryEmail, -// primaryEmailStatus: dbUser -// ? dbUser.primary_email_status -// : EmailStatusCode.EMAIL_UNSET, -// canCreateEmail: user.authorizations.canCreateEmail, -// hasPublicServiceEmail: -// dbUser && -// dbUser.primary_email && -// !dbUser.primary_email.includes(config.domain), -// domain: config.domain, -// secondaryEmail, -// }); -// } catch (e) {} -// } diff --git a/src/app/api/member/actions/createEmailForUser.ts b/src/app/api/member/actions/createEmailForUser.ts new file mode 100644 index 000000000..4e29059a7 --- /dev/null +++ b/src/app/api/member/actions/createEmailForUser.ts @@ -0,0 +1,31 @@ +"use server"; + +import { getServerSession } from "next-auth/next"; + +import { createEmailForUser } from "@/server/controllers/usersController/createEmailForUser"; +import { authOptions } from "@/utils/authoptions"; +import { + AuthorizationError, + UnwrapPromise, + withErrorHandling, +} from "@/utils/error"; + +export async function createEmail({ + username, + to_email, +}: { + username: string; + to_email?: string; +}) { + const session = await getServerSession(authOptions); + if (!session || !session.user.id) { + throw new AuthorizationError(); + } + + await createEmailForUser({ username }, session.user.id); +} + +export const safeCreateEmail = withErrorHandling< + UnwrapPromise>, + Parameters +>(createEmail); diff --git a/src/app/api/member/actions/createRedirectionForUser.ts b/src/app/api/member/actions/createRedirectionForUser.ts new file mode 100644 index 000000000..daa34853e --- /dev/null +++ b/src/app/api/member/actions/createRedirectionForUser.ts @@ -0,0 +1,80 @@ +"use server"; + +import { getServerSession } from "next-auth/next"; + +import { addEvent } from "@/lib/events"; +import { EventCode } from "@/models/actionEvent/actionEvent"; +import betagouv from "@/server/betagouv"; +import config from "@/server/config"; +import { buildBetaEmail, userInfos } from "@/server/controllers/utils"; +import { authOptions } from "@/utils/authoptions"; +import { + AuthorizationError, + UnwrapPromise, + withErrorHandling, +} from "@/utils/error"; + +export async function createRedirectionForUser({ + username, + to_email, + keep_copy, +}: { + username: string; + to_email: string; + keep_copy?: boolean; +}) { + const session = await getServerSession(authOptions); + if (!session || !session.user.id) { + throw new AuthorizationError(); + } + const isCurrentUser = session.user.id === username; + const user = await userInfos({ username }, isCurrentUser); + + // TODO: généraliser ce code dans un `app.param("id")` ? + if (!user.userInfos) { + throw new Error( + `Le membre ${username} n'a pas de fiche membre : vous ne pouvez pas créer de redirection.` + ); + } + + if (user.isExpired) { + throw new Error(`Le compte du membre ${username} est expiré.`); + } + + if (!user.authorizations.canCreateRedirection) { + throw new Error("Vous n'avez pas le droit de créer de redirection."); + } + + console.log( + `Création d'une redirection d'email id=${session.user.id}&from_email=${username}&to_email=${to_email}&keep_copy=${keep_copy}` + ); + + const secretariatUrl = `${config.protocol}://${config.host}`; + + const message = `À la demande de ${session.user.id} sur <${secretariatUrl}>, je crée une redirection mail pour ${username}`; + + try { + await addEvent({ + action_code: EventCode.MEMBER_REDIRECTION_CREATED, + created_by_username: session.user.id, + action_on_username: username, + action_metadata: { + value: to_email, + }, + }); + await betagouv.sendInfoToChat(message); + await betagouv.createRedirection( + buildBetaEmail(username), + to_email, + keep_copy === true + ); + } catch (err) { + console.log(err); + throw new Error(`Erreur pour créer la redirection: ${err}`); + } +} + +export const safeCreateRedirectionForUser = withErrorHandling< + UnwrapPromise>, + Parameters +>(createRedirectionForUser); diff --git a/src/app/api/member/actions/deleteRedirectionForUser.ts b/src/app/api/member/actions/deleteRedirectionForUser.ts new file mode 100644 index 000000000..1669cca84 --- /dev/null +++ b/src/app/api/member/actions/deleteRedirectionForUser.ts @@ -0,0 +1,66 @@ +"use server"; + +import { getServerSession } from "next-auth/next"; + +import { addEvent } from "@/lib/events"; +import { EventCode } from "@/models/actionEvent/actionEvent"; +import betagouv from "@/server/betagouv"; +import config from "@/server/config"; +import { buildBetaEmail, userInfos } from "@/server/controllers/utils"; +import { authOptions } from "@/utils/authoptions"; +import { + AuthorizationError, + UnwrapPromise, + withErrorHandling, +} from "@/utils/error"; + +export async function deleteRedirectionForUser({ + username, + toEmail, +}: { + username: string; + toEmail: string; +}) { + const session = await getServerSession(authOptions); + if (!session || !session.user.id) { + throw new AuthorizationError(); + } + const isCurrentUser = session.user.id === username; + const user = await userInfos({ username }, isCurrentUser); + + // TODO: vérifier si le membre existe sur Github ? + + if (!user.authorizations.canCreateRedirection) { + throw new Error( + "Vous n'avez pas le droit de supprimer cette redirection." + ); + } + + console.log( + `Suppression de la redirection by=${username}&to_email=${toEmail}` + ); + + const secretariatUrl = `${config.protocol}://${config.host}`; + + const message = `À la demande de ${session.user.id} sur <${secretariatUrl}>, je supprime la redirection mail de ${username} vers ${toEmail}`; + + try { + await addEvent({ + action_code: EventCode.MEMBER_REDIRECTION_DELETED, + created_by_username: session.user.id, + action_on_username: username, + action_metadata: { + value: toEmail, + }, + }); + await betagouv.sendInfoToChat(message); + await betagouv.deleteRedirection(buildBetaEmail(username), toEmail); + } catch (err) { + throw new Error(`Erreur pour supprimer la redirection: ${err}`); + } +} + +export const safeDeleteRedirectionForUser = withErrorHandling< + UnwrapPromise>, + Parameters +>(deleteRedirectionForUser); diff --git a/src/app/api/member/actions.ts b/src/app/api/member/actions/index.ts similarity index 63% rename from src/app/api/member/actions.ts rename to src/app/api/member/actions/index.ts index 9f5a4872e..f9965573b 100644 --- a/src/app/api/member/actions.ts +++ b/src/app/api/member/actions/index.ts @@ -1,7 +1,7 @@ "use server"; -import * as Sentry from "@sentry/nextjs"; import { revalidatePath } from "next/cache"; +import { cookies } from "next/headers"; import { getServerSession } from "next-auth/next"; import { addEvent } from "@/lib/events"; @@ -9,6 +9,7 @@ import { db } from "@/lib/kysely"; import { createMission, updateMission } from "@/lib/kysely/queries/missions"; import { getUserBasicInfo, getUserInfos } from "@/lib/kysely/queries/users"; import { getUserByEmail, MattermostUser, searchUsers } from "@/lib/mattermost"; +import * as mattermost from "@/lib/mattermost"; import { EventCode } from "@/models/actionEvent/actionEvent"; import { updateMemberMissionsSchemaType } from "@/models/actions/member"; import { UpdateOvhResponder } from "@/models/actions/ovh"; @@ -18,6 +19,7 @@ import { } from "@/models/mapper"; import { CommunicationEmailCode, + EmailStatusCode, memberWrapperPublicInfoSchemaType, } from "@/models/member"; import betagouv from "@/server/betagouv"; @@ -28,7 +30,13 @@ import { addContactsToMailingLists, removeContactsFromMailingList, } from "@/server/config/email.config"; -import { capitalizeWords, userInfos } from "@/server/controllers/utils"; +import { + capitalizeWords, + isPublicServiceEmail, + isAdminEmail, + userInfos, + buildBetaEmail, +} from "@/server/controllers/utils"; import { Contact, MAILING_LIST_TYPE } from "@/server/modules/email"; import { authOptions } from "@/utils/authoptions"; import { @@ -38,6 +46,8 @@ import { ValidationError, OVHError, withErrorHandling, + AdminEmailNotAllowedError, + BusinessError, } from "@/utils/error"; async function changeSecondaryEmailForUser( @@ -339,6 +349,206 @@ async function updateMemberMissions( revalidatePath("/community/[id]", "layout"); } +export async function managePrimaryEmailForUser({ + username, + primaryEmail, +}: { + username: string; + primaryEmail: string; +}): Promise { + const session = await getServerSession(authOptions); + if (!session || !session.user.id) { + throw new AuthorizationError(); + } + const isCurrentUser = session?.user.id === username; + const user = await userInfos({ username }, isCurrentUser); + if (!user.authorizations.canChangeEmails) { + throw new AuthorizationError(); + } + const primaryEmailIsPublicServiceEmail = await isPublicServiceEmail( + primaryEmail + ); + if (!primaryEmailIsPublicServiceEmail) { + throw new BusinessError( + `L'email renseigné n'est pas un email de service public` + ); + } + if (isAdminEmail(primaryEmail)) { + throw new AdminEmailNotAllowedError(); + } + + if (user.userInfos.primary_email?.includes(config.domain)) { + await betagouv.createRedirection( + user.userInfos.primary_email, + primaryEmail, + false + ); + try { + await betagouv.deleteEmail( + user.userInfos.primary_email.split("@")[0] + ); + } catch (e) { + console.log(e, "Email is possibly already deleted"); + } + } else { + try { + await mattermost.getUserByEmail(primaryEmail); + } catch { + throw new BusinessError( + `L'email n'existe pas dans mattermost, pour utiliser cette adresse comme adresse principale ton compte mattermost doit aussi utiliser cette adresse.` + ); + } + } + await db + .updateTable("users") + .where("username", "=", username) + .set({ + primary_email: primaryEmail, + username, + }) + .execute(); + + await addEvent({ + action_code: EventCode.MEMBER_PRIMARY_EMAIL_UPDATED, + created_by_username: session.user.id, + action_on_username: username, + action_metadata: { + value: primaryEmail, + old_value: user + ? user.userInfos.primary_email || undefined + : undefined, + }, + }); + + console.log(`${session?.user.id} a mis à jour son adresse mail primaire.`); + revalidatePath("/community/[id]", "layout"); + return; +} + +export async function deleteEmailForUser({ username }: { username: string }) { + const session = await getServerSession(authOptions); + if (!session || !session.user.id) { + throw new AuthorizationError(); + } + const isCurrentUser = session.user.id === username; + + try { + const user = await userInfos({ username }, isCurrentUser); + if (!isCurrentUser && !user.isExpired) { + throw new BusinessError( + `Le compte "${username}" n'est pas expiré, vous ne pouvez pas supprimer ce compte.` + ); + } + + await betagouv.sendInfoToChat( + `Suppression de compte de ${username} (à la demande de ${session.user.id})` + ); + await addEvent({ + action_code: EventCode.MEMBER_EMAIL_DELETED, + created_by_username: session.user.id, + action_on_username: username, + }); + if (user.emailRedirections && user.emailRedirections.length > 0) { + await betagouv.requestRedirections( + "DELETE", + user.emailRedirections.map((x) => x.id) + ); + console.log( + `Suppression des redirections de l'email de ${username} (à la demande de ${session.user.id})` + ); + } + + await betagouv.createRedirection( + buildBetaEmail(username), + config.leavesEmail, + false + ); + await db + .updateTable("users") + .set({ + secondary_email: null, + primary_email: null, + primary_email_status: EmailStatusCode.EMAIL_UNSET, + }) + .where("username", "=", username) + .execute(); + console.log( + `Redirection des emails de ${username} vers ${config.leavesEmail} (à la demande de ${session.user.id})` + ); + if (isCurrentUser) { + cookies().set("next-auth.session-token", "", { + maxAge: -1, + path: "/", + }); + // Optionally, clear other cookies related to authentication + cookies().set("__Secure-next-auth.session-token", "", { + maxAge: -1, + path: "/", + }); + } + } catch (err) { + console.error(err); + } +} + +export async function manageSecondaryEmailForUser({ + username, + secondaryEmail, +}: { + username: string; + secondaryEmail: string; +}) { + const session = await getServerSession(authOptions); + if (!session || !session.user.id) { + throw new AuthorizationError(); + } + const isCurrentUser = session.user.id === username; + + const user = await userInfos({ username }, isCurrentUser); + if (!isCurrentUser && !user.isExpired) { + throw new BusinessError( + `Le compte "${username}" n'est pas expiré, vous ne pouvez pas supprimer ce compte.` + ); + } + + if ( + user.authorizations.canChangeEmails || + config.ESPACE_MEMBRE_ADMIN.includes(session.user.id) + ) { + const user = await db + .selectFrom("users") + .select("secondary_email") + .where("username", "=", username) + .executeTakeFirst(); + + if (!user) { + throw new BusinessError("Users not found"); + } + + await db + .updateTable("users") + .set({ + secondary_email: secondaryEmail, + }) + .where("username", "=", username) + .execute(); + + await addEvent({ + action_code: EventCode.MEMBER_SECONDARY_EMAIL_UPDATED, + created_by_username: session.user.id, + action_on_username: username, + action_metadata: { + value: secondaryEmail, + old_value: user.secondary_email || "", + }, + }); + + console.log( + `${session.user.id} a mis à jour son adresse mail secondaire.` + ); + } +} + export const safeUpdateMemberMissions = withErrorHandling(updateMemberMissions); export const safeGetUserPublicInfo = withErrorHandling< UnwrapPromise>, @@ -360,3 +570,15 @@ export const safeUpdateCommunicationEmail = withErrorHandling< UnwrapPromise>, Parameters >(updateCommunicationEmail); +export const safeManagePrimaryEmailForUser = withErrorHandling< + UnwrapPromise>, + Parameters +>(managePrimaryEmailForUser); +export const safeDeleteEmailForUser = withErrorHandling< + UnwrapPromise>, + Parameters +>(deleteEmailForUser); +export const safeManageSecondaryEmailForUser = withErrorHandling< + UnwrapPromise>, + Parameters +>(manageSecondaryEmailForUser); diff --git a/src/app/api/member/actions/updatePasswordForUser.ts b/src/app/api/member/actions/updatePasswordForUser.ts new file mode 100644 index 000000000..3a93bf9a3 --- /dev/null +++ b/src/app/api/member/actions/updatePasswordForUser.ts @@ -0,0 +1,98 @@ +"use server"; + +import { getServerSession } from "next-auth/next"; + +import { addEvent } from "@/lib/events"; +import { db } from "@/lib/kysely"; +import { EventCode } from "@/models/actionEvent/actionEvent"; +import { EmailStatusCode } from "@/models/member"; +import betagouv from "@/server/betagouv"; +import config from "@/server/config"; +import { buildBetaEmail, userInfos } from "@/server/controllers/utils"; +import { authOptions } from "@/utils/authoptions"; +import { + AuthorizationError, + UnwrapPromise, + withErrorHandling, +} from "@/utils/error"; + +export async function updatePasswordForUser({ + username, + new_password, +}: { + username: string; + new_password: string; +}) { + const session = await getServerSession(authOptions); + if (!session || !session.user.id) { + throw new AuthorizationError(); + } + const isCurrentUser = session.user.id === username; + const user = await userInfos({ username }, isCurrentUser); + + if (!user.userInfos) { + throw new Error( + `Le membre ${username} n'a pas de fiche sur l'espace-membre : vous ne pouvez pas modifier le mot de passe.` + ); + } + + if (user.isExpired) { + throw new Error(`Le compte du membre ${username} est expiré.`); + } + + if (!user.authorizations.canChangePassword) { + throw new Error("Vous n'avez pas le droit de changer le mot de passe."); + } + + const password = new_password; + + if ( + !password || + password.length < 9 || + password.length > 30 || + password !== password.trim() + ) { + throw new Error( + "Le mot de passe doit comporter de 9 à 30 caractères, ne pas contenir d'accents ni d'espace au début ou à la fin." + ); + } + const email = buildBetaEmail(username); + + console.log( + `Changement de mot de passe by=${session.user.id}&email=${email}` + ); + + const secretariatUrl = `${config.protocol}://${config.host}`; + await betagouv.changePassword( + username, + password, + user.emailInfos?.emailPlan + ); + await addEvent({ + action_code: EventCode.MEMBER_PASSWORD_UPDATED, + created_by_username: session.user.id, + action_on_username: username, + }); + if ( + [ + EmailStatusCode.EMAIL_SUSPENDED, + EmailStatusCode.EMAIL_ACTIVE_AND_PASSWORD_DEFINITION_PENDING, + ].includes(user.userInfos.primary_email_status) + ) { + await db + .updateTable("users") + .where("username", "=", username) + .set({ + primary_email_status: EmailStatusCode.EMAIL_ACTIVE, + primary_email_status_updated_at: new Date(), + }) + .execute(); + } + const message = `À la demande de ${session.user.id} sur <${secretariatUrl}>, je change le mot de passe pour ${username}.`; + await betagouv.sendInfoToChat(message); +} + +export const safeUpdatePasswordForUser = withErrorHandling< + UnwrapPromise>, + Parameters +>(updatePasswordForUser); diff --git a/src/app/api/member/actions/upgradeEmailForUser.ts b/src/app/api/member/actions/upgradeEmailForUser.ts new file mode 100644 index 000000000..cf285f4b3 --- /dev/null +++ b/src/app/api/member/actions/upgradeEmailForUser.ts @@ -0,0 +1,83 @@ +"use server"; + +import { getServerSession } from "next-auth/next"; + +import { addEvent } from "@/lib/events"; +import { EventCode } from "@/models/actionEvent/actionEvent"; +import { EMAIL_PLAN_TYPE } from "@/models/ovh"; +import betagouv from "@/server/betagouv"; +import config from "@/server/config"; +import { buildBetaEmail, userInfos } from "@/server/controllers/utils"; +import { authOptions } from "@/utils/authoptions"; +import { + AuthorizationError, + UnwrapPromise, + withErrorHandling, +} from "@/utils/error"; + +export async function upgradeEmailForUser({ + username, + password, +}: { + username: string; + password: string; +}) { + const session = await getServerSession(authOptions); + if (!session || !session.user.id) { + throw new AuthorizationError(); + } + const isCurrentUser = session.user.id === username; + + if (!config.ESPACE_MEMBRE_ADMIN.includes(session.user.id)) { + throw new Error( + `Vous n'etes pas admin vous ne pouvez pas upgrade ce compte.` + ); + } + if (!config.OVH_EMAIL_PRO_NAME) { + throw new Error(`OVH email pro account is not defined`); + } + + const availableProEmail: string[] = + await betagouv.getAvailableProEmailInfos(); + if (!availableProEmail.length) { + throw new Error(` + Il n'y a plus d'email pro disponible + `); + } + const user = await userInfos({ username }, isCurrentUser); + + if (user.isExpired) { + throw new Error( + `Le compte "${username}" est expiré, vous ne pouvez pas upgrade ce compte.` + ); + } + + if (!user.emailInfos) { + throw new Error(`Le compte "${username}" n'a pas de compte email`); + } + + if (user.emailInfos.emailPlan === EMAIL_PLAN_TYPE.EMAIL_PLAN_PRO) { + throw new Error(`Le compte "${username}" est déjà un compte pro.`); + } + + await betagouv.migrateEmailAccount({ + userId: user.emailInfos.email.split("@")[0], + destinationEmailAddress: availableProEmail[0], + destinationServiceName: config.OVH_EMAIL_PRO_NAME, + password, + }); + + await betagouv.sendInfoToChat( + `Upgrade de compte de ${username} (à la demande de ${session.user.id})` + ); + addEvent({ + action_code: EventCode.MEMBER_EMAIL_UPGRADED, + created_by_username: session.user.id, + action_on_username: username, + }); +} + +export const safeUpgradeEmailForUser = withErrorHandling< + UnwrapPromise>, + Parameters +>(upgradeEmailForUser); diff --git a/src/components/AdminMattermostPage/AdminMattermost.tsx b/src/components/AdminMattermostPage/AdminMattermost.tsx index 1c6a0817c..1bc12a2be 100644 --- a/src/components/AdminMattermostPage/AdminMattermost.tsx +++ b/src/components/AdminMattermostPage/AdminMattermost.tsx @@ -9,6 +9,9 @@ import Select from "@codegouvfr/react-dsfr/Select"; import axios from "axios"; import { AdminMattermostUser } from "./AdminMattermostUser"; +import { safeGetMattermostUsersInfo } from "@/app/api/admin/actions/getMattermostUsersInfo"; +import { safeSendMessageToUsersOnChat } from "@/app/api/admin/actions/sendMattermostMessage"; +import { MattermostUser } from "@/lib/mattermost"; import { memberBaseInfoSchemaType } from "@/models/member"; import routes, { computeRoute } from "@/routes/routes"; @@ -32,7 +35,9 @@ const css = ".panel { min-height: 400px; }"; // to have enough space to display /* Pure component */ export const AdminMattermost = (props: AdminMattermostProps) => { - const [usersForMessage, setUsersForMessage] = useState([]); + const [usersForMessage, setUsersForMessage] = useState( + [] + ); const [channel, setChannel] = useState("town-square"); const [messageType, setMessageType] = useState("channel"); const [includeEmails, setIncludeEmails] = useState(""); @@ -62,24 +67,24 @@ export const AdminMattermost = (props: AdminMattermostProps) => { const updateQuery = async () => { const params = { - excludeEmails, - includeEmails, + excludeEmails: excludeEmails.split(",").filter((email) => email), + includeEmails: includeEmails.split(",").filter((email) => email), fromBeta, }; const queryParamsString = Object.keys(params) .map((key) => key + "=" + params[key]) .join("&"); try { - const usersForMessage = await axios - .get( - `${computeRoute( - routes.ADMIN_MATTERMOST_MESSAGE_API - )}?${queryParamsString}`, - { - withCredentials: true, - } - ) - .then((resp) => resp.data.users); + const usersForMessage = await safeGetMattermostUsersInfo( + params + ).then((resp) => { + if (resp.success) { + return resp.data.users; + } else { + alert("Impossible de récupérer la liste d'utilisateur"); + } + return []; + }); setUsersForMessage(usersForMessage); } catch (e) {} }; @@ -88,8 +93,8 @@ export const AdminMattermost = (props: AdminMattermostProps) => { return { fromBeta, prod, - includeEmails, - excludeEmails, + includeEmails: includeEmails.split(",").filter((email) => email), + excludeEmails: excludeEmails.split(",").filter((email) => email), text, channel: messageType === "channel" ? channel : undefined, }; @@ -110,28 +115,24 @@ export const AdminMattermost = (props: AdminMattermostProps) => { }` ) === true ) { - const res = await axios.post( - computeRoute(routes.ADMIN_MATTERMOST_SEND_MESSAGE), - buildParams(true), - { - withCredentials: true, - } - ); - alert(`${res.data.message}`); + const res = await safeSendMessageToUsersOnChat(buildParams(true)); + if (res.success) { + alert(`Message envoyé`); + } else { + alert(`Echec de l'envoie du message`); + } } else { alert(`Le message n'a pas été envoyé`); } }; const sendTest = async () => { try { - const res = await axios.post( - computeRoute(routes.ADMIN_MATTERMOST_SEND_MESSAGE), - buildParams(false), - { - withCredentials: true, - } - ); - alert(`${res.data.message}`); + const res = await safeSendMessageToUsersOnChat(buildParams(false)); + if (res.success) { + alert(`Message envoyé`); + } else { + alert(`Echec de l'envoie du message`); + } } catch (e) { console.error("Erreur"); } diff --git a/src/components/MemberPage/CreateEmailForm.tsx b/src/components/MemberPage/CreateEmailForm.tsx index 2deab41e7..e18236958 100644 --- a/src/components/MemberPage/CreateEmailForm.tsx +++ b/src/components/MemberPage/CreateEmailForm.tsx @@ -1,8 +1,12 @@ -import { memberBaseInfoSchemaType } from "@/models/member"; -import routes, { computeRoute } from "@/routes/routes"; +import { useState } from "react"; + import { Alert } from "@codegouvfr/react-dsfr/Alert"; import { Button } from "@codegouvfr/react-dsfr/Button"; -import { useState } from "react"; + +import { safeCreateEmail } from "@/app/api/member/actions/createEmailForUser"; +import { memberBaseInfoSchemaType } from "@/models/member"; +import routes, { computeRoute } from "@/routes/routes"; +import { createEmailForUser } from "@/server/controllers/usersController/createEmailForUser"; export const CreateEmailForm = ({ userInfos, @@ -20,26 +24,15 @@ export const CreateEmailForm = ({ const onSubmit = async (e) => { e.preventDefault(); try { - const response = await fetch( - computeRoute( - routes.USER_CREATE_EMAIL_API.replace( - ":username", - userInfos.username - ) - ), - { - method: "POST", - credentials: "include", // This is equivalent to `withCredentials: true` - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - } - ); - - if (!response.ok) { - const body = await response.json(); - throw new Error(body.errors); + const response = await safeCreateEmail({ + username: userInfos.username, + }); + if (!response.success) { + setAlertMessage({ + title: `Une erreur est survenue`, + message: `${response.message}`, + type: "warning", + }); } setAlertMessage({ diff --git a/src/components/MemberPage/Email/BlocChangerMotDePasse.tsx b/src/components/MemberPage/Email/BlocChangerMotDePasse.tsx index c1da1b42e..2fc341678 100644 --- a/src/components/MemberPage/Email/BlocChangerMotDePasse.tsx +++ b/src/components/MemberPage/Email/BlocChangerMotDePasse.tsx @@ -10,6 +10,10 @@ import Button from "@codegouvfr/react-dsfr/Button"; import { fr } from "@codegouvfr/react-dsfr/fr"; import axios from "axios"; +import { + safeUpdatePasswordForUser, + updatePasswordForUser, +} from "@/app/api/member/actions/updatePasswordForUser"; import { EmailStatusCode, memberBaseInfoSchemaType } from "@/models/member"; import routes, { computeRoute } from "@/routes/routes"; @@ -21,44 +25,32 @@ function PasswordChange({ username }: { username: string }) { message: NonNullable; type: AlertProps.Severity; }>(); - const onSubmit = (e) => { + const onSubmit = async (e) => { e.preventDefault(); setIsSaving(true); setAlertMessage(undefined); - axios - .post( - computeRoute( - routes.USER_UPDATE_PASSWORD_API.replace( - ":username", - username - ) - ), - { - new_password: password, - }, - { - withCredentials: true, - } - ) - .then(() => { - setTimeout(() => { - // timeout to let user understand that function ran - setIsSaving(false); - setAlertMessage({ - title: `Mot de passe mis à jour avec succès`, - message: `Ton mot de passe a été mis à jour, tu peux maintenant l'utiliser sur le webmail ou dans ton client email.`, - type: "success", - }); - }, 1000); - }) - .catch((err) => { + const resp = await safeUpdatePasswordForUser({ + username, + new_password: password, + }); + if (resp.success) { + setTimeout(() => { + // timeout to let user understand that function ran setIsSaving(false); setAlertMessage({ - title: "Une erreur est survenue", - message: `Réessayer plus tard, si l'erreur persiste contacter espace-membre@beta.gouv.fr. Erreur : ${err?.response?.data?.error}`, - type: "warning", + title: `Mot de passe mis à jour avec succès`, + message: `Ton mot de passe a été mis à jour, tu peux maintenant l'utiliser sur le webmail ou dans ton client email.`, + type: "success", }); + }, 1000); + } else { + setIsSaving(false); + setAlertMessage({ + title: "Une erreur est survenue", + message: `Réessayer plus tard, si l'erreur persiste contacter espace-membre@beta.gouv.fr. Erreur : ${resp.message}`, + type: "warning", }); + } }; const onPasswordChange = (e) => { setPassword(e.target.value); diff --git a/src/components/MemberPage/Email/BlocConfigurerEmailPrincipal.tsx b/src/components/MemberPage/Email/BlocConfigurerEmailPrincipal.tsx index 9db09c7f9..28780e486 100644 --- a/src/components/MemberPage/Email/BlocConfigurerEmailPrincipal.tsx +++ b/src/components/MemberPage/Email/BlocConfigurerEmailPrincipal.tsx @@ -6,6 +6,10 @@ import Button from "@codegouvfr/react-dsfr/Button"; import Input from "@codegouvfr/react-dsfr/Input"; import axios from "axios"; +import { + managePrimaryEmailForUser, + safeManagePrimaryEmailForUser, +} from "@/app/api/member/actions"; import { memberBaseInfoSchemaType } from "@/models/member"; import routes, { computeRoute } from "@/routes/routes"; @@ -41,32 +45,23 @@ export default function BlocConfigurerEmailPrincipal({ {canChangeEmails && (
{ + onSubmit={async (e) => { e.preventDefault(); const confirmed = confirm( "Êtes-vous vraiment certain(e) de vouloir changer cet email ?" ); if (confirmed) { setIsSaving(true); - axios - .put( - computeRoute( - routes.USER_UPDATE_PRIMARY_EMAIL_API - ).replace(":username", userInfos.username), - { - primaryEmail: value, - }, - { - withCredentials: true, - } - ) - .then((resp) => { - setIsSaving(false); - }) - .catch((err) => { - setIsSaving(false); - console.error(err); - }); + const resp = await safeManagePrimaryEmailForUser({ + username: userInfos.username, + primaryEmail: value, + }); + if (resp.success) { + alert("Ton email a bien été mis à jour."); + } else { + alert(resp.message); + } + setIsSaving(false); } }} > diff --git a/src/components/MemberPage/Email/BlocCreateEmail.tsx b/src/components/MemberPage/Email/BlocCreateEmail.tsx index b84f76a99..93df0bfff 100644 --- a/src/components/MemberPage/Email/BlocCreateEmail.tsx +++ b/src/components/MemberPage/Email/BlocCreateEmail.tsx @@ -5,6 +5,10 @@ import Button from "@codegouvfr/react-dsfr/Button"; import Input from "@codegouvfr/react-dsfr/Input"; import axios from "axios"; +import { + createEmail, + safeCreateEmail, +} from "@/app/api/member/actions/createEmailForUser"; import { memberSchemaType } from "@/models/member"; import routes, { computeRoute } from "@/routes/routes"; @@ -26,19 +30,22 @@ export default function BlocCreateEmail({ className="no-margin" onSubmit={async (e) => { e.preventDefault(); - try { - await axios.post( - computeRoute(routes.USER_CREATE_EMAIL_API).replace( - ":username", - userInfos.username - ), - {}, - { - withCredentials: true, - } - ); + const resp = await safeCreateEmail({ + username: userInfos.username, + }); + // await axios.post( + // computeRoute(routes.USER_CREATE_EMAIL_API).replace( + // ":username", + // userInfos.username + // ), + // {}, + // { + // withCredentials: true, + // } + // ); + if (resp.success) { alert("Ton email a bien été créé."); - } catch (e) { + } else { alert( `Ton email n'a pas pu être créé suite à une erreur.` ); diff --git a/src/components/MemberPage/Email/BlocRedirection.tsx b/src/components/MemberPage/Email/BlocRedirection.tsx index b989d8fa8..89c8e4350 100644 --- a/src/components/MemberPage/Email/BlocRedirection.tsx +++ b/src/components/MemberPage/Email/BlocRedirection.tsx @@ -6,12 +6,16 @@ import Button from "@codegouvfr/react-dsfr/Button"; import { Checkbox } from "@codegouvfr/react-dsfr/Checkbox"; import { Input } from "@codegouvfr/react-dsfr/Input"; import axios from "axios"; +import { use } from "chai"; +import { safeCreateRedirectionForUser } from "@/app/api/member/actions/createRedirectionForUser"; +import { safeDeleteRedirectionForUser } from "@/app/api/member/actions/deleteRedirectionForUser"; import { memberBaseInfoSchemaType, memberWrapperSchemaType, } from "@/models/member"; import routes, { computeRoute } from "@/routes/routes"; +import { userInfos } from "@/server/controllers/utils"; export default function BlocRedirection({ redirections, @@ -46,23 +50,16 @@ export default function BlocRedirection({ className="redirection-form" method="POST" onSubmit={async (e) => { - e.preventDefault(); - try { - await axios.delete( - computeRoute( - routes.USER_DELETE_REDIRECTION_API - ) - .replace( - ":username", - userInfos.username - ) - .replace( - ":email", - redirection.to - ), - { withCredentials: true } - ); - } catch (e) {} + const resp = + await safeDeleteRedirectionForUser({ + username: userInfos.username, + toEmail: redirection.to, + }); + if (resp.success) { + alert("Redirection supprimée"); + } else { + alert("Erreur lors de la suppression"); + } }} >