Skip to content

Commit 1309e0f

Browse files
authored
fix(frontend): fix string encoding with login credentials (#253)
1 parent be751f3 commit 1309e0f

2 files changed

Lines changed: 47 additions & 2 deletions

File tree

frontend/server/helpers/dav.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,25 @@ describe('dav helpers', () => {
215215
expect(result).toBe(false)
216216
})
217217

218+
it('lowercases the email and requests a case-insensitive collation', async () => {
219+
// The stored vCard EMAIL may be mixed-case and admin paths hand through raw
220+
// input, so the filter must normalize the query and pin the collation rather
221+
// than relying on the DAV server default (see findUserByEmail).
222+
vi.mocked(addressBookQuery).mockResolvedValue([])
223+
const account = createCardDAVAccount(config)
224+
await findUserByEmail(account, 'Max@Example.COM')
225+
const callArgs = vi.mocked(addressBookQuery).mock.calls[0]![0] as Record<string, unknown>
226+
const filters = callArgs.filters as {
227+
'prop-filter': {
228+
_attributes: { name: string }
229+
'text-match': { _attributes: { collation: string }; _text: string }
230+
}
231+
}
232+
expect(filters['prop-filter']._attributes.name).toBe('EMAIL')
233+
expect(filters['prop-filter']['text-match']._text).toBe('max@example.com')
234+
expect(filters['prop-filter']['text-match']._attributes.collation).toBe('i;unicode-casemap')
235+
})
236+
218237
it('returns false when result has missing or empty addressData', async () => {
219238
// Hits the defensive `if (typeof data !== 'string' || data.length === 0)`
220239
// branch — Baikal can return a single hit but with no `address-data`
@@ -273,6 +292,21 @@ describe('dav helpers', () => {
273292
const result = await findUserByToken(account, 'bad-token')
274293
expect(result).toBe(false)
275294
})
295+
296+
it('matches the token exactly without a collation', async () => {
297+
// Login tokens are high-entropy and case-sensitive: the filter must NOT relax
298+
// matching with a collation the way the EMAIL lookup does.
299+
vi.mocked(addressBookQuery).mockResolvedValue([])
300+
const account = createCardDAVAccount(config)
301+
await findUserByToken(account, 'AbC-ToKeN')
302+
const callArgs = vi.mocked(addressBookQuery).mock.calls[0]![0] as Record<string, unknown>
303+
const filters = callArgs.filters as {
304+
'prop-filter': { _attributes: { name: string }; 'text-match': unknown }
305+
}
306+
expect(filters['prop-filter']._attributes.name).toBe(X_LOGIN_TOKEN)
307+
// Plain string payload => no `collation` attribute, value passed verbatim.
308+
expect(filters['prop-filter']['text-match']).toBe('AbC-ToKeN')
309+
})
276310
})
277311

278312
describe('saveUser', () => {

frontend/server/helpers/dav.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ async function findUserByProperty(
133133
account: DAVAccount,
134134
propertyName: string,
135135
propertyValue: string,
136+
// CardDAV text-match without an explicit collation falls back to the server
137+
// default. RFC 6352 says that default is `i;unicode-casemap` (case-insensitive),
138+
// but relying on the server to honor it is fragile. Pass a collation explicitly
139+
// where case-insensitivity matters (e.g. EMAIL) and leave it undefined for
140+
// high-entropy values (e.g. login tokens) that must match exactly.
141+
collation?: string,
136142
) {
137143
const users = await addressBookQuery({
138144
url: account.homeUrl!,
@@ -147,7 +153,9 @@ async function findUserByProperty(
147153
_attributes: {
148154
name: propertyName,
149155
},
150-
['text-match']: propertyValue,
156+
['text-match']: collation
157+
? { _attributes: { collation }, _text: propertyValue }
158+
: propertyValue,
151159
},
152160
},
153161
fetchOptions: getFetchOptions(),
@@ -171,8 +179,11 @@ async function findUserByProperty(
171179
export const findUserByToken = async (account: DAVAccount, token: string) =>
172180
findUserByProperty(account, X_LOGIN_TOKEN, token)
173181

182+
// Single choke-point for email normalization: callers may pass any casing
183+
// (admin endpoints/CLIs hand through raw input), so lowercase here and request a
184+
// case-insensitive match against the (possibly mixed-case) stored vCard EMAIL.
174185
export const findUserByEmail = async (account: DAVAccount, email: string) =>
175-
findUserByProperty(account, 'EMAIL', email)
186+
findUserByProperty(account, 'EMAIL', email.toLowerCase(), 'i;unicode-casemap')
176187

177188
export const saveUser = async (account: DAVAccount, user: DAVResponse, vcard: ICAL.Component) =>
178189
updateVCard({

0 commit comments

Comments
 (0)