diff --git a/api/server/services/AuthService.js b/api/server/services/AuthService.js index 1792de66db..0098e54124 100644 --- a/api/server/services/AuthService.js +++ b/api/server/services/AuthService.js @@ -129,7 +129,7 @@ const verifyEmail = async (req) => { return { message: 'Email already verified', status: 'success' }; } - let emailVerificationData = await findToken({ email: decodedEmail }); + let emailVerificationData = await findToken({ email: decodedEmail }, { sort: { createdAt: -1 } }); if (!emailVerificationData) { logger.warn(`[verifyEmail] [No email verification data found] [Email: ${decodedEmail}]`); @@ -319,9 +319,12 @@ const requestPasswordReset = async (req) => { * @returns */ const resetPassword = async (userId, token, password) => { - let passwordResetToken = await findToken({ - userId, - }); + let passwordResetToken = await findToken( + { + userId, + }, + { sort: { createdAt: -1 } }, + ); if (!passwordResetToken) { return new Error('Invalid or expired password reset token'); diff --git a/packages/data-schemas/src/methods/token.spec.ts b/packages/data-schemas/src/methods/token.spec.ts index 2480619984..bec18a419e 100644 --- a/packages/data-schemas/src/methods/token.spec.ts +++ b/packages/data-schemas/src/methods/token.spec.ts @@ -181,6 +181,197 @@ describe('Token Methods - Detailed Tests', () => { expect(found).toBeNull(); }); + + test('should find most recent token with sort option', async () => { + const recentUserId = new mongoose.Types.ObjectId(); + + // Create tokens with different timestamps + const oldDate = new Date(Date.now() - 7200000); // 2 hours ago + const midDate = new Date(Date.now() - 3600000); // 1 hour ago + const newDate = new Date(); // now + + await Token.create([ + { + token: 'old-token', + userId: recentUserId, + email: 'recent@example.com', + createdAt: oldDate, + expiresAt: new Date(oldDate.getTime() + 86400000), + }, + { + token: 'mid-token', + userId: recentUserId, + email: 'recent@example.com', + createdAt: midDate, + expiresAt: new Date(midDate.getTime() + 86400000), + }, + { + token: 'new-token', + userId: recentUserId, + email: 'recent@example.com', + createdAt: newDate, + expiresAt: new Date(newDate.getTime() + 86400000), + }, + ]); + + // Find most recent token for the user with sort option + const found = await methods.findToken( + { userId: recentUserId.toString() }, + { sort: { createdAt: -1 } }, + ); + + expect(found).toBeDefined(); + expect(found?.token).toBe('new-token'); + expect(found?.createdAt.getTime()).toBe(newDate.getTime()); + }); + + test('should find oldest token with ascending sort', async () => { + const sortUserId = new mongoose.Types.ObjectId(); + + const oldDate = new Date(Date.now() - 7200000); + const midDate = new Date(Date.now() - 3600000); + const newDate = new Date(); + + await Token.create([ + { + token: 'sort-old', + userId: sortUserId, + email: 'sort@example.com', + createdAt: oldDate, + expiresAt: new Date(oldDate.getTime() + 86400000), + }, + { + token: 'sort-mid', + userId: sortUserId, + email: 'sort@example.com', + createdAt: midDate, + expiresAt: new Date(midDate.getTime() + 86400000), + }, + { + token: 'sort-new', + userId: sortUserId, + email: 'sort@example.com', + createdAt: newDate, + expiresAt: new Date(newDate.getTime() + 86400000), + }, + ]); + + // Find oldest token with ascending sort + const found = await methods.findToken( + { userId: sortUserId.toString() }, + { sort: { createdAt: 1 } }, + ); + + expect(found).toBeDefined(); + expect(found?.token).toBe('sort-old'); + expect(found?.createdAt.getTime()).toBe(oldDate.getTime()); + }); + + test('should handle multiple sort criteria', async () => { + const multiSortUserId = new mongoose.Types.ObjectId(); + const sameDate = new Date(); + + await Token.create([ + { + token: 'token-a', + userId: multiSortUserId, + email: 'z@example.com', + createdAt: sameDate, + expiresAt: new Date(sameDate.getTime() + 86400000), + }, + { + token: 'token-b', + userId: multiSortUserId, + email: 'a@example.com', + createdAt: sameDate, + expiresAt: new Date(sameDate.getTime() + 86400000), + }, + { + token: 'token-c', + userId: multiSortUserId, + email: 'm@example.com', + createdAt: new Date(Date.now() - 1000), // slightly older + expiresAt: new Date(Date.now() + 86400000), + }, + ]); + + // Sort by createdAt descending, then by email ascending + const found = await methods.findToken( + { userId: multiSortUserId.toString() }, + { sort: { createdAt: -1, email: 1 } }, + ); + + expect(found).toBeDefined(); + // Should get token-b (same recent date but 'a@example.com' comes first alphabetically) + expect(found?.token).toBe('token-b'); + expect(found?.email).toBe('a@example.com'); + }); + + test('should find token with projection option', async () => { + const projectionUserId = new mongoose.Types.ObjectId(); + + await Token.create({ + token: 'projection-token', + userId: projectionUserId, + email: 'projection@example.com', + identifier: 'oauth-projection', + createdAt: new Date(), + expiresAt: new Date(Date.now() + 86400000), + }); + + // Find token with projection to only include specific fields + const found = await methods.findToken( + { userId: projectionUserId.toString() }, + { projection: { token: 1, email: 1 } }, + ); + + expect(found).toBeDefined(); + expect(found?.token).toBe('projection-token'); + expect(found?.email).toBe('projection@example.com'); + // Note: _id is usually included by default unless explicitly excluded + }); + + test('should respect combined query options', async () => { + const combinedUserId = new mongoose.Types.ObjectId(); + + // Create multiple tokens with different attributes + await Token.create([ + { + token: 'combined-1', + userId: combinedUserId, + email: 'combined1@example.com', + createdAt: new Date(Date.now() - 7200000), + expiresAt: new Date(Date.now() + 86400000), + }, + { + token: 'combined-2', + userId: combinedUserId, + email: 'combined2@example.com', + createdAt: new Date(Date.now() - 3600000), + expiresAt: new Date(Date.now() + 86400000), + }, + { + token: 'combined-3', + userId: combinedUserId, + email: 'combined3@example.com', + createdAt: new Date(), + expiresAt: new Date(Date.now() + 86400000), + }, + ]); + + // Use multiple query options together + const found = await methods.findToken( + { userId: combinedUserId.toString() }, + { + sort: { createdAt: -1 }, + projection: { token: 1, createdAt: 1 }, + }, + ); + + expect(found).toBeDefined(); + expect(found?.token).toBe('combined-3'); // Most recent + expect(found?.createdAt).toBeDefined(); + }); }); describe('updateToken', () => { diff --git a/packages/data-schemas/src/methods/token.ts b/packages/data-schemas/src/methods/token.ts index c3c862b96a..5dfee869c0 100644 --- a/packages/data-schemas/src/methods/token.ts +++ b/packages/data-schemas/src/methods/token.ts @@ -1,3 +1,4 @@ +import type { QueryOptions } from 'mongoose'; import { IToken, TokenCreateData, TokenQuery, TokenUpdateData, TokenDeleteResult } from '~/types'; import logger from '~/config/winston'; @@ -81,7 +82,7 @@ export function createTokenMethods(mongoose: typeof import('mongoose')) { /** * Finds a Token document that matches the provided query. */ - async function findToken(query: TokenQuery): Promise { + async function findToken(query: TokenQuery, options?: QueryOptions): Promise { try { const Token = mongoose.models.Token; const conditions = []; @@ -99,9 +100,7 @@ export function createTokenMethods(mongoose: typeof import('mongoose')) { conditions.push({ identifier: query.identifier }); } - const token = await Token.findOne({ - $and: conditions, - }).lean(); + const token = await Token.findOne({ $and: conditions }, null, options).lean(); return token as IToken | null; } catch (error) {