From a08f9df52dbdd8e49279451c2ce30571bd7a0de8 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Thu, 10 Jul 2025 15:41:51 +0200 Subject: [PATCH] impr: store emailVerified status in database (@fehmer) --- .../__tests__/api/controllers/user.spec.ts | 60 +++++++++++++++++++ backend/__tests__/dal/user.spec.ts | 7 ++- backend/__tests__/middlewares/auth.spec.ts | 3 + backend/src/api/controllers/user.ts | 12 ++++ backend/src/dal/user.ts | 10 +++- backend/src/middlewares/auth.ts | 2 + packages/schemas/src/users.ts | 1 + 7 files changed, 92 insertions(+), 3 deletions(-) diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts index f32d40dbe..c948a326a 100644 --- a/backend/__tests__/api/controllers/user.spec.ts +++ b/backend/__tests__/api/controllers/user.spec.ts @@ -253,6 +253,66 @@ describe("user controller test", () => { }); }); }); + describe("getUser", () => { + const getUserMock = vi.spyOn(UserDal, "getUser"); + const updateEmailMock = vi.spyOn(UserDal, "updateEmail"); + beforeEach(() => { + getUserMock.mockReset(); + updateEmailMock.mockReset(); + }); + it("should update emailVerified if undefined", async () => { + //GIVEN + getUserMock.mockResolvedValue({ + uid, + email: "old", + } as any); + mockAuth.modifyToken({ email_verified: false, email: "old" }); + + //WHEN + await mockApp + .get("/users") + .set("Authorization", `Bearer ${uid}`) + .expect(200); + + //THEN + expect(updateEmailMock).toHaveBeenCalledWith(uid, "old", false); + }); + it("should update emailVerified if changed", async () => { + //GIVEN + getUserMock.mockResolvedValue({ + uid, + emailVerified: false, + email: "old", + } as any); + mockAuth.modifyToken({ email_verified: true, email: "next" }); + + //WHEN + await mockApp + .get("/users") + .set("Authorization", `Bearer ${uid}`) + .expect(200); + + //THEN + expect(updateEmailMock).toHaveBeenCalledWith(uid, "next", true); + }); + it("should not update emailVerified if not changed", async () => { + //GIVEN + getUserMock.mockResolvedValue({ + uid, + emailVerified: false, + } as any); + mockAuth.modifyToken({ email_verified: false }); + + //WHEN + await mockApp + .get("/users") + .set("Authorization", `Bearer ${uid}`) + .expect(200); + + //THEN + expect(updateEmailMock).not.toHaveBeenCalled(); + }); + }); describe("sendVerificationEmail", () => { const adminGetUserMock = vi.fn(); const adminGenerateVerificationLinkMock = vi.fn(); diff --git a/backend/__tests__/dal/user.spec.ts b/backend/__tests__/dal/user.spec.ts index d763a06ef..aa75bdeff 100644 --- a/backend/__tests__/dal/user.spec.ts +++ b/backend/__tests__/dal/user.spec.ts @@ -102,6 +102,7 @@ describe("UserDal", () => { expect(insertedUser.email).toBe(newUser.email); expect(insertedUser.uid).toBe(newUser.uid); expect(insertedUser.name).toBe(newUser.name); + expect(insertedUser.emailVerified).toBe(false); }); it("should error if the user already exists", async () => { @@ -1167,7 +1168,10 @@ describe("UserDal", () => { }); it("should update", async () => { //given - const { uid } = await UserTestData.createUser({ email: "init" }); + const { uid } = await UserTestData.createUser({ + email: "init", + emailVerified: true, + }); //when await expect(UserDAL.updateEmail(uid, "next")).resolves.toBe(true); @@ -1175,6 +1179,7 @@ describe("UserDal", () => { //then const read = await UserDAL.getUser(uid, "read"); expect(read.email).toEqual("next"); + expect(read.emailVerified).toEqual(false); }); }); describe("resetPb", () => { diff --git a/backend/__tests__/middlewares/auth.spec.ts b/backend/__tests__/middlewares/auth.spec.ts index 378714ae0..e7f892b7e 100644 --- a/backend/__tests__/middlewares/auth.spec.ts +++ b/backend/__tests__/middlewares/auth.spec.ts @@ -20,6 +20,7 @@ const mockDecodedToken: DecodedIdToken = { uid: "123456789", email: "newuser@mail.com", iat: 0, + email_verified: true, } as DecodedIdToken; vi.spyOn(AuthUtils, "verifyIdToken").mockResolvedValue(mockDecodedToken); @@ -62,6 +63,7 @@ describe("middlewares/auth", () => { type: "None", uid: "", email: "", + emailVerified: false, }, }, }; @@ -122,6 +124,7 @@ describe("middlewares/auth", () => { expect(decodedToken?.type).toBe("Bearer"); expect(decodedToken?.email).toBe(mockDecodedToken.email); expect(decodedToken?.uid).toBe(mockDecodedToken.uid); + expect(decodedToken?.emailVerified).toBe(mockDecodedToken.email_verified); expect(nextFunction).toHaveBeenCalledOnce(); expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("Bearer"); diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 348b66724..9380c25f3 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -605,6 +605,18 @@ export async function getUser(req: MonkeyRequest): Promise { ); delete relevantUserInfo.customThemes; + //update users emailVerified status if it changed + const { email, emailVerified } = req.ctx.decodedToken; + if (emailVerified !== undefined && emailVerified !== userInfo.emailVerified) { + void addImportantLog( + "user_verify_email", + `emailVerified changed to ${emailVerified} for email ${email}`, + uid + ); + await UserDAL.updateEmail(uid, email, emailVerified); + userInfo.emailVerified = emailVerified; + } + const userData: User = { ...relevantUserInfo, resultFilterPresets, diff --git a/backend/src/dal/user.ts b/backend/src/dal/user.ts index 17b0615ad..a2bb9cc20 100644 --- a/backend/src/dal/user.ts +++ b/backend/src/dal/user.ts @@ -87,6 +87,7 @@ export async function addUser( custom: {}, }, testActivity: {}, + emailVerified: false, }; const result = await getUsersCollection().updateOne( @@ -231,9 +232,14 @@ export async function updateQuoteRatings( export async function updateEmail( uid: string, - email: string + email: string, + emailVerified: boolean = true ): Promise { - await updateUser({ uid }, { $set: { email } }, { stack: "update email" }); + await updateUser( + { uid }, + { $set: { email, emailVerified } }, + { stack: "update email" } + ); return true; } diff --git a/backend/src/middlewares/auth.ts b/backend/src/middlewares/auth.ts index 3c04c981c..e3999050c 100644 --- a/backend/src/middlewares/auth.ts +++ b/backend/src/middlewares/auth.ts @@ -26,6 +26,7 @@ export type DecodedToken = { type: "Bearer" | "ApeKey" | "None" | "GithubWebhook"; uid: string; email: string; + emailVerified?: boolean; }; const DEFAULT_OPTIONS: RequestAuthenticationOptions = { @@ -189,6 +190,7 @@ async function authenticateWithBearerToken( type: "Bearer", uid: decodedToken.uid, email: decodedToken.email ?? "", + emailVerified: decodedToken.email_verified, }; } catch (error) { if ( diff --git a/packages/schemas/src/users.ts b/packages/schemas/src/users.ts index a72ecca0f..9c0baee76 100644 --- a/packages/schemas/src/users.ts +++ b/packages/schemas/src/users.ts @@ -271,6 +271,7 @@ export const UserSchema = z.object({ quoteMod: QuoteModSchema.optional(), resultFilterPresets: z.array(ResultFiltersSchema).optional(), testActivity: TestActivitySchema.optional(), + emailVerified: z.boolean().optional(), }); export type User = z.infer;