diff --git a/packages/api/src/auth/invite.spec.ts b/packages/api/src/auth/invite.spec.ts new file mode 100644 index 0000000000..6acd59e821 --- /dev/null +++ b/packages/api/src/auth/invite.spec.ts @@ -0,0 +1,51 @@ +import mongoose from 'mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { createMethods, createModels } from '@librechat/data-schemas'; +import { createInvite, getInvite } from './invite'; + +let mongoServer: MongoMemoryServer; +let deps: Parameters[2]; + +beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); + createModels(mongoose); + + const { createToken, findToken } = createMethods(mongoose); + deps = { createToken, findToken } as Parameters[2]; +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); +}); + +beforeEach(async () => { + await mongoose.models.Token.deleteMany({}); +}); + +describe('getInvite', () => { + it('returns the invite for the address it was issued to', async () => { + const token = (await createInvite('pedro@example.com', deps)) as string; + + await expect(getInvite(token, 'pedro@example.com', deps)).resolves.toMatchObject({ + email: 'pedro@example.com', + }); + }); + + it('refuses an address the invite was not issued to', async () => { + const token = (await createInvite('pedro@example.com', deps)) as string; + + await expect(getInvite(token, 'someone.else@example.com', deps)).resolves.toMatchObject({ + error: true, + }); + }); + + it('refuses a lookup carrying no address at all', async () => { + const token = (await createInvite('pedro@example.com', deps)) as string; + + await expect(getInvite(token, undefined as unknown as string, deps)).resolves.toMatchObject({ + error: true, + }); + }); +}); diff --git a/packages/api/src/auth/invite.ts b/packages/api/src/auth/invite.ts index 19e1e54b46..2806b8cdd8 100644 --- a/packages/api/src/auth/invite.ts +++ b/packages/api/src/auth/invite.ts @@ -45,6 +45,13 @@ export async function getInvite( deps: InviteDeps, ): Promise { try { + /** `findToken` builds its query from the fields it is given, so an absent email + * is a lookup by token alone — which would match the invite and let the caller + * consume it without ever proving the address. */ + if (!email) { + throw new Error('Invite not found or email does not match'); + } + const token = decodeURIComponent(encodedToken); const hash = await hashToken(token); const invite = await deps.findToken({ token: hash, email });