mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-21 17:44:11 +00:00
📧 fix: Normalize Token Emails on Write, as Every Read Already Does (#15536)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
* 📧 fix: Normalize Token Emails on Write, as Every Read Already Does `findToken` and `deleteTokens` both trim and lowercase the email before querying — the comment on `findToken` even documents it as automatic — but nothing normalized the write. A token created with mixed case or surrounding whitespace could therefore never be found or deleted again: an invite issued to `User@Example.com` was unredeemable. Fixed on the schema rather than in a caller. `createInvite` is the caller that surfaced it (#15529), but the asymmetry belongs to `Token`, so every writer had the same trap and any new one would inherit it. `User.email` is already declared `lowercase: true` in this package, so this only brings `Token.email` in line with the convention beside it — and a schema-level setter cannot be bypassed the way a helper can. Existing documents are not migrated. A token already stored with mixed case stays unfindable, exactly as it is today; only new writes are corrected. Fixes #15529 * ✏️ fix: Correct the Schema Comment and Name the No-Email Test for What It Asserts Both from Copilot's review. `User.email` sets `lowercase` but not `trim`, so claiming this "mirrors" it overstated the parallel — this change trims as well, because the read side does. The comment now states the `findToken`/`deleteTokens` contract it actually follows. The no-email test was named for `null` while omitting the field and asserting `undefined`. Renamed to match, and a real null case added: `TokenCreateData.email` is `string | undefined` so null is only reachable by writing the model directly, but both read methods branch on `email === null`, so it is worth proving the new setters leave that value alone.
This commit is contained in:
parent
d346e5ab5e
commit
458c473d9e
2 changed files with 92 additions and 0 deletions
|
|
@ -1105,3 +1105,88 @@ describe('Token Methods - Detailed Tests', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Token email normalization', () => {
|
||||
const userId = new mongoose.Types.ObjectId();
|
||||
|
||||
const createFor = (email: string) =>
|
||||
methods.createToken({
|
||||
userId,
|
||||
email,
|
||||
token: 'hashed-token-value',
|
||||
expiresIn: 3600,
|
||||
});
|
||||
|
||||
it('stores the email normalized so reads can find it again', async () => {
|
||||
await createFor('User@Example.COM');
|
||||
|
||||
const stored = await Token.findOne({ userId });
|
||||
expect(stored?.email).toBe('user@example.com');
|
||||
});
|
||||
|
||||
it('trims surrounding whitespace on write', async () => {
|
||||
await createFor(' spaced@example.com ');
|
||||
|
||||
const stored = await Token.findOne({ userId });
|
||||
expect(stored?.email).toBe('spaced@example.com');
|
||||
});
|
||||
|
||||
it('finds a token created with mixed case, whatever case the lookup uses', async () => {
|
||||
await createFor('User@Example.COM');
|
||||
|
||||
for (const lookup of ['User@Example.COM', 'user@example.com', 'USER@EXAMPLE.COM']) {
|
||||
const found = await methods.findToken({ token: 'hashed-token-value', email: lookup });
|
||||
expect(found).not.toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('finds a token created with padding when the lookup is clean', async () => {
|
||||
await createFor(' spaced@example.com ');
|
||||
|
||||
const found = await methods.findToken({
|
||||
token: 'hashed-token-value',
|
||||
email: 'spaced@example.com',
|
||||
});
|
||||
|
||||
expect(found).not.toBeNull();
|
||||
});
|
||||
|
||||
it('deletes a token created with mixed case', async () => {
|
||||
await createFor('User@Example.COM');
|
||||
|
||||
const result = await methods.deleteTokens({ email: 'user@example.com' });
|
||||
|
||||
expect(result.deletedCount).toBe(1);
|
||||
expect(await Token.countDocuments({ userId })).toBe(0);
|
||||
});
|
||||
|
||||
it('leaves a token written without an email alone', async () => {
|
||||
await methods.createToken({
|
||||
userId,
|
||||
token: 'no-email-token',
|
||||
expiresIn: 3600,
|
||||
});
|
||||
|
||||
const stored = await Token.findOne({ userId });
|
||||
expect(stored?.email).toBeUndefined();
|
||||
});
|
||||
|
||||
it('leaves an explicitly null email as null, which the read side matches on', async () => {
|
||||
/** `TokenCreateData.email` is `string | undefined`, so a null is only reachable by
|
||||
* writing the model directly — but `findToken` and `deleteTokens` both branch on
|
||||
* `email === null`, so the setters must not coerce it into something else. */
|
||||
await Token.create({
|
||||
userId,
|
||||
email: null,
|
||||
token: 'null-email-token',
|
||||
createdAt: new Date(),
|
||||
expiresAt: new Date(Date.now() + 3600000),
|
||||
});
|
||||
|
||||
const stored = await Token.findOne({ userId });
|
||||
expect(stored?.email).toBeNull();
|
||||
|
||||
const found = await methods.findToken({ token: 'null-email-token', email: null });
|
||||
expect(found).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ const tokenSchema: Schema<IToken> = new Schema({
|
|||
},
|
||||
email: {
|
||||
type: String,
|
||||
/** `findToken` and `deleteTokens` both apply `.trim().toLowerCase()` to the email
|
||||
* before querying, so the write side has to match on both counts or a token
|
||||
* created with mixed case or surrounding whitespace can never be found again —
|
||||
* an invite issued to `User@Example.com` was unredeemable. `User.email` sets
|
||||
* `lowercase` alone; the read contract here also trims, so this adds `trim`. */
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue