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 { 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(
|
||||||
|
|
|
@ -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}`)}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|
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). */
|
/** 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.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in a new issue