impr: store emailVerified status in database (@fehmer)

This commit is contained in:
Christian Fehmer 2025-07-10 15:41:51 +02:00
parent 6dad5415c2
commit a08f9df52d
No known key found for this signature in database
GPG key ID: FE53784A69964062
7 changed files with 92 additions and 3 deletions

View file

@ -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();

View file

@ -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", () => {

View file

@ -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");

View file

@ -605,6 +605,18 @@ export async function getUser(req: MonkeyRequest): Promise<GetUserResponse> {
);
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,

View file

@ -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<boolean> {
await updateUser({ uid }, { $set: { email } }, { stack: "update email" });
await updateUser(
{ uid },
{ $set: { email, emailVerified } },
{ stack: "update email" }
);
return true;
}

View file

@ -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 (

View file

@ -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<typeof UserSchema>;