mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): case insensitive usernames (#5170)
* feat(core): case insensitive usernames * test(core): add case insensitive username test cases --------- Co-authored-by: Tc001 <tc001@t0.lv>
This commit is contained in:
parent
b4390aeae6
commit
acb7fd3fea
4 changed files with 70 additions and 2 deletions
6
.changeset/fresh-lies-breathe.md
Normal file
6
.changeset/fresh-lies-breathe.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@logto/shared": patch
|
||||
"@logto/core": patch
|
||||
---
|
||||
|
||||
Add case sensitive username env variable
|
|
@ -1,8 +1,10 @@
|
|||
import { Users } from '@logto/schemas';
|
||||
import { convertToIdentifiers } from '@logto/shared';
|
||||
import Sinon from 'sinon';
|
||||
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
||||
|
||||
import { mockUser } from '#src/__mocks__/index.js';
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { DeletionError } from '#src/errors/SlonikError/index.js';
|
||||
import type { QueryType } from '#src/utils/test-utils.js';
|
||||
import { expectSqlAssert } from '#src/utils/test-utils.js';
|
||||
|
@ -33,7 +35,17 @@ const {
|
|||
deleteUserIdentity,
|
||||
} = createUserQueries(pool);
|
||||
|
||||
const stubIsCaseSensitiveUsername = (isCaseSensitiveUsername: boolean) =>
|
||||
Sinon.stub(EnvSet, 'values').value({
|
||||
...EnvSet.values,
|
||||
isCaseSensitiveUsername,
|
||||
});
|
||||
|
||||
describe('user query', () => {
|
||||
beforeEach(() => {
|
||||
stubIsCaseSensitiveUsername(true);
|
||||
});
|
||||
|
||||
const { table, fields } = convertToIdentifiers(Users);
|
||||
const databaseValue = {
|
||||
...mockUser,
|
||||
|
@ -60,6 +72,24 @@ describe('user query', () => {
|
|||
await expect(findUserByUsername(mockUser.username!)).resolves.toEqual(databaseValue);
|
||||
});
|
||||
|
||||
it('findUserByUsername (case insensitive)', async () => {
|
||||
stubIsCaseSensitiveUsername(false);
|
||||
const expectSql = sql`
|
||||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
from ${table}
|
||||
where lower(${fields.username})=lower($1)
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([mockUser.username]);
|
||||
|
||||
return createMockQueryResult([databaseValue]);
|
||||
});
|
||||
|
||||
await expect(findUserByUsername(mockUser.username!)).resolves.toEqual(databaseValue);
|
||||
});
|
||||
|
||||
it('findUserByEmail', async () => {
|
||||
const expectSql = sql`
|
||||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
|
@ -132,6 +162,26 @@ describe('user query', () => {
|
|||
await expect(hasUser(mockUser.username!)).resolves.toEqual(true);
|
||||
});
|
||||
|
||||
it('hasUser (case insensitive)', async () => {
|
||||
stubIsCaseSensitiveUsername(false);
|
||||
const expectSql = sql`
|
||||
SELECT EXISTS(
|
||||
select ${fields.id}
|
||||
from ${table}
|
||||
where lower(${fields.username})=lower($1)
|
||||
)
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([mockUser.username]);
|
||||
|
||||
return createMockQueryResult([{ exists: true }]);
|
||||
});
|
||||
|
||||
await expect(hasUser(mockUser.username!)).resolves.toEqual(true);
|
||||
});
|
||||
|
||||
it('hasUserWithId', async () => {
|
||||
const expectSql = sql`
|
||||
SELECT EXISTS(
|
||||
|
|
|
@ -7,6 +7,7 @@ import type { CommonQueryMethods } from 'slonik';
|
|||
import { sql } from 'slonik';
|
||||
|
||||
import { buildUpdateWhereWithPool } from '#src/database/update-where.js';
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import { DeletionError } from '#src/errors/SlonikError/index.js';
|
||||
import type { Search } from '#src/utils/search.js';
|
||||
import { buildConditionsFromSearch } from '#src/utils/search.js';
|
||||
|
@ -63,7 +64,11 @@ export const createUserQueries = (pool: CommonQueryMethods) => {
|
|||
pool.maybeOne<User>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
from ${table}
|
||||
where ${fields.username}=${username}
|
||||
${
|
||||
EnvSet.values.isCaseSensitiveUsername
|
||||
? sql`where ${fields.username}=${username}`
|
||||
: sql`where lower(${fields.username})=lower(${username})`
|
||||
}
|
||||
`);
|
||||
|
||||
const findUserByEmail = async (email: string) =>
|
||||
|
@ -100,7 +105,11 @@ export const createUserQueries = (pool: CommonQueryMethods) => {
|
|||
pool.exists(sql`
|
||||
select ${fields.id}
|
||||
from ${table}
|
||||
where ${fields.username}=${username}
|
||||
${
|
||||
EnvSet.values.isCaseSensitiveUsername
|
||||
? sql`where ${fields.username}=${username}`
|
||||
: sql`where lower(${fields.username})=lower(${username})`
|
||||
}
|
||||
${conditionalSql(excludeUserId, (id) => sql`and ${fields.id}<>${id}`)}
|
||||
`);
|
||||
|
||||
|
|
3
packages/shared/src/node/env/GlobalValues.ts
vendored
3
packages/shared/src/node/env/GlobalValues.ts
vendored
|
@ -104,6 +104,9 @@ export default class GlobalValues {
|
|||
/** Maximum number of clients to keep in a single database pool (i.e. per `Tenant` class). */
|
||||
public readonly databasePoolSize = Number(getEnv('DATABASE_POOL_SIZE', '20'));
|
||||
|
||||
/** Case insensitive username */
|
||||
public readonly isCaseSensitiveUsername = yes(getEnv('CASE_SENSITIVE_USERNAME', 'true'));
|
||||
|
||||
/**
|
||||
* The Redis endpoint (optional). If it's set, the central cache mechanism will be automatically enabled.
|
||||
*
|
||||
|
|
Loading…
Reference in a new issue