0
Fork 0
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:
wangsijie 2023-12-28 10:57:47 +08:00 committed by GitHub
parent b4390aeae6
commit acb7fd3fea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 70 additions and 2 deletions

View file

@ -0,0 +1,6 @@
---
"@logto/shared": patch
"@logto/core": patch
---
Add case sensitive username env variable

View file

@ -1,8 +1,10 @@
import { Users } from '@logto/schemas'; import { Users } from '@logto/schemas';
import { convertToIdentifiers } from '@logto/shared'; import { convertToIdentifiers } from '@logto/shared';
import Sinon from 'sinon';
import { createMockPool, createMockQueryResult, sql } from 'slonik'; import { createMockPool, createMockQueryResult, sql } from 'slonik';
import { mockUser } from '#src/__mocks__/index.js'; import { mockUser } from '#src/__mocks__/index.js';
import { EnvSet } from '#src/env-set/index.js';
import { DeletionError } from '#src/errors/SlonikError/index.js'; import { DeletionError } from '#src/errors/SlonikError/index.js';
import type { QueryType } from '#src/utils/test-utils.js'; import type { QueryType } from '#src/utils/test-utils.js';
import { expectSqlAssert } from '#src/utils/test-utils.js'; import { expectSqlAssert } from '#src/utils/test-utils.js';
@ -33,7 +35,17 @@ const {
deleteUserIdentity, deleteUserIdentity,
} = createUserQueries(pool); } = createUserQueries(pool);
const stubIsCaseSensitiveUsername = (isCaseSensitiveUsername: boolean) =>
Sinon.stub(EnvSet, 'values').value({
...EnvSet.values,
isCaseSensitiveUsername,
});
describe('user query', () => { describe('user query', () => {
beforeEach(() => {
stubIsCaseSensitiveUsername(true);
});
const { table, fields } = convertToIdentifiers(Users); const { table, fields } = convertToIdentifiers(Users);
const databaseValue = { const databaseValue = {
...mockUser, ...mockUser,
@ -60,6 +72,24 @@ describe('user query', () => {
await expect(findUserByUsername(mockUser.username!)).resolves.toEqual(databaseValue); 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 () => { it('findUserByEmail', async () => {
const expectSql = sql` const expectSql = sql`
select ${sql.join(Object.values(fields), sql`,`)} select ${sql.join(Object.values(fields), sql`,`)}
@ -132,6 +162,26 @@ describe('user query', () => {
await expect(hasUser(mockUser.username!)).resolves.toEqual(true); 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 () => { it('hasUserWithId', async () => {
const expectSql = sql` const expectSql = sql`
SELECT EXISTS( SELECT EXISTS(

View file

@ -7,6 +7,7 @@ import type { CommonQueryMethods } from 'slonik';
import { sql } from 'slonik'; import { sql } from 'slonik';
import { buildUpdateWhereWithPool } from '#src/database/update-where.js'; 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 { DeletionError } from '#src/errors/SlonikError/index.js';
import type { Search } from '#src/utils/search.js'; import type { Search } from '#src/utils/search.js';
import { buildConditionsFromSearch } 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` pool.maybeOne<User>(sql`
select ${sql.join(Object.values(fields), sql`,`)} select ${sql.join(Object.values(fields), sql`,`)}
from ${table} 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) => const findUserByEmail = async (email: string) =>
@ -100,7 +105,11 @@ export const createUserQueries = (pool: CommonQueryMethods) => {
pool.exists(sql` pool.exists(sql`
select ${fields.id} select ${fields.id}
from ${table} 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}`)} ${conditionalSql(excludeUserId, (id) => sql`and ${fields.id}<>${id}`)}
`); `);

View file

@ -104,6 +104,9 @@ export default class GlobalValues {
/** Maximum number of clients to keep in a single database pool (i.e. per `Tenant` class). */ /** 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')); 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. * The Redis endpoint (optional). If it's set, the central cache mechanism will be automatically enabled.
* *