🪙 refactor: Auth Token Retrieval with Sorting and Query Options (#9884)

This commit is contained in:
Danny Avila 2025-09-29 09:06:40 -04:00 committed by GitHub
parent a1471c2f37
commit c0eb19730a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 201 additions and 8 deletions

View file

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

View file

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

View file

@ -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<IToken | null> {
async function findToken(query: TokenQuery, options?: QueryOptions): Promise<IToken | null> {
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) {