mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
feat(core): set user default roles from env (#1793)
This commit is contained in:
parent
84c0d8f845
commit
4afdf3cb4c
13 changed files with 88 additions and 76 deletions
|
@ -23,7 +23,6 @@ const loadEnvValues = async () => {
|
|||
const port = Number(getEnv('PORT', '3001'));
|
||||
const localhostUrl = `${isHttpsEnabled ? 'https' : 'http'}://localhost:${port}`;
|
||||
const endpoint = getEnv('ENDPOINT', localhostUrl);
|
||||
const additionalConnectorPackages = getEnvAsStringArray('ADDITIONAL_CONNECTOR_PACKAGES', []);
|
||||
|
||||
return Object.freeze({
|
||||
isTest,
|
||||
|
@ -35,7 +34,8 @@ const loadEnvValues = async () => {
|
|||
port,
|
||||
localhostUrl,
|
||||
endpoint,
|
||||
additionalConnectorPackages,
|
||||
additionalConnectorPackages: getEnvAsStringArray('ADDITIONAL_CONNECTOR_PACKAGES'),
|
||||
userDefaultRoleNames: getEnvAsStringArray('USER_DEFAULT_ROLE_NAMES'),
|
||||
developmentUserId: getEnv('DEVELOPMENT_USER_ID'),
|
||||
trustProxyHeader: isTrue(getEnv('TRUST_PROXY_HEADER')),
|
||||
oidc: await loadOidcValues(appendPath(endpoint, '/oidc').toString()),
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { User, UsersPasswordEncryptionMethod } from '@logto/schemas';
|
||||
import { User, CreateUser, Users, UsersPasswordEncryptionMethod } from '@logto/schemas';
|
||||
import { argon2Verify } from 'hash-wasm';
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
import { buildInsertInto } from '@/database/insert-into';
|
||||
import envSet from '@/env-set';
|
||||
import { findRolesByRoleNames, insertRoles } from '@/queries/roles';
|
||||
import { findUserByUsername, hasUserWithId, updateUserById } from '@/queries/user';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
import { buildIdGenerator } from '@/utils/id';
|
||||
|
@ -58,3 +61,30 @@ export const findUserByUsernameAndPassword = async (
|
|||
|
||||
export const updateLastSignInAt = async (userId: string) =>
|
||||
updateUserById(userId, { lastSignInAt: Date.now() });
|
||||
|
||||
const insertUserQuery = buildInsertInto<CreateUser, User>(Users, {
|
||||
returning: true,
|
||||
});
|
||||
|
||||
// Temp solution since Hasura requires a role to proceed authn.
|
||||
// The source of default roles should be guarded and moved to database once we implement RBAC.
|
||||
export const insertUser: typeof insertUserQuery = async ({ roleNames, ...rest }) => {
|
||||
const computedRoleNames = [
|
||||
...new Set((roleNames ?? []).concat(envSet.values.userDefaultRoleNames)),
|
||||
];
|
||||
|
||||
if (computedRoleNames.length > 0) {
|
||||
const existingRoles = await findRolesByRoleNames(computedRoleNames);
|
||||
const missingRoleNames = computedRoleNames.filter(
|
||||
(roleName) => !existingRoles.some(({ name }) => roleName === name)
|
||||
);
|
||||
|
||||
if (missingRoleNames.length > 0) {
|
||||
await insertRoles(
|
||||
missingRoleNames.map((name) => ({ name, description: 'User default role.' }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return insertUserQuery({ roleNames: computedRoleNames, ...rest });
|
||||
};
|
||||
|
|
|
@ -18,3 +18,12 @@ export const findRolesByRoleNames = async (roleNames: string[]) =>
|
|||
from ${table}
|
||||
where ${fields.name} in (${sql.join(roleNames, sql`, `)})
|
||||
`);
|
||||
|
||||
export const insertRoles = async (roles: Role[]) =>
|
||||
envSet.pool.query(sql`
|
||||
insert into ${table} (${fields.name}, ${fields.description}) values
|
||||
${sql.join(
|
||||
roles.map(({ name, description }) => sql`(${name}, ${description})`),
|
||||
sql`, `
|
||||
)}
|
||||
`);
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
hasUserWithEmail,
|
||||
hasUserWithIdentity,
|
||||
hasUserWithPhone,
|
||||
insertUser,
|
||||
countUsers,
|
||||
findUsers,
|
||||
updateUserById,
|
||||
|
@ -235,33 +234,6 @@ describe('user query', () => {
|
|||
await expect(hasUserWithIdentity(target, mockUser.id)).resolves.toEqual(true);
|
||||
});
|
||||
|
||||
it('insertUser', async () => {
|
||||
const expectSql = sql`
|
||||
insert into ${table} (${sql.join(Object.values(fields), sql`, `)})
|
||||
values (${sql.join(
|
||||
Object.values(fields)
|
||||
.slice(0, -1)
|
||||
.map((_, index) => `$${index + 1}`),
|
||||
sql`, `
|
||||
)}, to_timestamp(${Object.values(fields).length}::double precision / 1000))
|
||||
returning *
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
|
||||
expect(values).toEqual(
|
||||
Users.fieldKeys.map((k) =>
|
||||
k === 'lastSignInAt' ? mockUser[k] : convertToPrimitiveOrSql(k, mockUser[k])
|
||||
)
|
||||
);
|
||||
|
||||
return createMockQueryResult([dbvalue]);
|
||||
});
|
||||
|
||||
await expect(insertUser(mockUser)).resolves.toEqual(dbvalue);
|
||||
});
|
||||
|
||||
it('countUsers', async () => {
|
||||
const search = 'foo';
|
||||
const expectSql = sql`
|
||||
|
|
|
@ -83,10 +83,6 @@ export const hasUserWithIdentity = async (target: string, userId: string) =>
|
|||
`
|
||||
);
|
||||
|
||||
export const insertUser = buildInsertInto<CreateUser, User>(Users, {
|
||||
returning: true,
|
||||
});
|
||||
|
||||
const buildUserSearchConditionSql = (search: string) => {
|
||||
const searchFields = [fields.primaryEmail, fields.primaryPhone, fields.username, fields.name];
|
||||
const conditions = searchFields.map((filedName) => sql`${filedName} like ${'%' + search + '%'}`);
|
||||
|
|
|
@ -39,12 +39,6 @@ jest.mock('@/queries/user', () => ({
|
|||
})
|
||||
),
|
||||
deleteUserById: jest.fn(),
|
||||
insertUser: jest.fn(
|
||||
async (user: CreateUser): Promise<User> => ({
|
||||
...mockUser,
|
||||
...user,
|
||||
})
|
||||
),
|
||||
deleteUserIdentity: jest.fn(),
|
||||
}));
|
||||
|
||||
|
@ -54,6 +48,12 @@ jest.mock('@/lib/user', () => ({
|
|||
passwordEncrypted: 'password',
|
||||
passwordEncryptionMethod: 'Argon2i',
|
||||
})),
|
||||
insertUser: jest.fn(
|
||||
async (user: CreateUser): Promise<User> => ({
|
||||
...mockUser,
|
||||
...user,
|
||||
})
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/queries/roles', () => ({
|
||||
|
|
|
@ -5,7 +5,7 @@ import pick from 'lodash.pick';
|
|||
import { literal, object, string } from 'zod';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { encryptUserPassword, generateUserId } from '@/lib/user';
|
||||
import { encryptUserPassword, generateUserId, insertUser } from '@/lib/user';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import koaPagination from '@/middleware/koa-pagination';
|
||||
import { findRolesByRoleNames } from '@/queries/roles';
|
||||
|
@ -16,7 +16,6 @@ import {
|
|||
countUsers,
|
||||
findUserById,
|
||||
hasUser,
|
||||
insertUser,
|
||||
updateUserById,
|
||||
} from '@/queries/user';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
|
|
@ -7,23 +7,26 @@ import { createRequester } from '@/utils/test-utils';
|
|||
|
||||
import sessionPasswordlessRoutes from './passwordless';
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
generateUserId: () => 'user1',
|
||||
updateLastSignInAt: async (...args: unknown[]) => updateUserById(...args),
|
||||
}));
|
||||
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
generateUserId: () => 'user1',
|
||||
updateLastSignInAt: async (...args: unknown[]) => updateUserById(...args),
|
||||
insertUser: async (...args: unknown[]) => insertUser(...args),
|
||||
}));
|
||||
|
||||
jest.mock('@/queries/user', () => ({
|
||||
findUserById: async () => findUserById(),
|
||||
findUserByPhone: async () => ({ id: 'id' }),
|
||||
findUserByEmail: async () => ({ id: 'id' }),
|
||||
insertUser: async (...args: unknown[]) => insertUser(...args),
|
||||
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
||||
hasUser: async (username: string) => username === 'username1',
|
||||
hasUserWithPhone: async (phone: string) => phone === '13000000000',
|
||||
hasUserWithEmail: async (email: string) => email === 'a@a.com',
|
||||
}));
|
||||
|
||||
const sendPasscode = jest.fn(async () => ({ connector: { id: 'connectorIdValue' } }));
|
||||
jest.mock('@/lib/passcode', () => ({
|
||||
createPasscode: async () => ({ id: 'id' }),
|
||||
|
|
|
@ -6,12 +6,11 @@ import { object, string } from 'zod';
|
|||
import RequestError from '@/errors/RequestError';
|
||||
import { createPasscode, sendPasscode, verifyPasscode } from '@/lib/passcode';
|
||||
import { assignInteractionResults } from '@/lib/session';
|
||||
import { generateUserId, updateLastSignInAt } from '@/lib/user';
|
||||
import { generateUserId, insertUser, updateLastSignInAt } from '@/lib/user';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import {
|
||||
hasUserWithEmail,
|
||||
hasUserWithPhone,
|
||||
insertUser,
|
||||
findUserByEmail,
|
||||
findUserByPhone,
|
||||
} from '@/queries/user';
|
||||
|
|
|
@ -8,6 +8,25 @@ import { createRequester } from '@/utils/test-utils';
|
|||
|
||||
import sessionRoutes from './session';
|
||||
|
||||
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
const hasActiveUsers = jest.fn(async () => true);
|
||||
|
||||
jest.mock('@/queries/user', () => ({
|
||||
findUserById: async () => findUserById(),
|
||||
findUserByIdentity: async () => ({ id: 'id', identities: {} }),
|
||||
findUserByPhone: async () => ({ id: 'id' }),
|
||||
findUserByEmail: async () => ({ id: 'id' }),
|
||||
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
||||
hasUser: async (username: string) => username === 'username1',
|
||||
hasUserWithIdentity: async (connectorId: string, userId: string) =>
|
||||
connectorId === 'connectorId' && userId === 'id',
|
||||
hasUserWithPhone: async (phone: string) => phone === '13000000000',
|
||||
hasUserWithEmail: async (email: string) => email === 'a@a.com',
|
||||
hasActiveUsers: async () => hasActiveUsers(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
async findUserByUsernameAndPassword(username: string, password: string) {
|
||||
if (username !== 'username' && username !== 'admin') {
|
||||
|
@ -28,25 +47,7 @@ jest.mock('@/lib/user', () => ({
|
|||
passwordEncryptionMethod: 'Argon2i',
|
||||
}),
|
||||
updateLastSignInAt: async (...args: unknown[]) => updateUserById(...args),
|
||||
}));
|
||||
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
const hasActiveUsers = jest.fn(async () => true);
|
||||
|
||||
jest.mock('@/queries/user', () => ({
|
||||
findUserById: async () => findUserById(),
|
||||
findUserByIdentity: async () => ({ id: 'id', identities: {} }),
|
||||
findUserByPhone: async () => ({ id: 'id' }),
|
||||
findUserByEmail: async () => ({ id: 'id' }),
|
||||
insertUser: async (...args: unknown[]) => insertUser(...args),
|
||||
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
||||
hasUser: async (username: string) => username === 'username1',
|
||||
hasUserWithIdentity: async (connectorId: string, userId: string) =>
|
||||
connectorId === 'connectorId' && userId === 'id',
|
||||
hasUserWithPhone: async (phone: string) => phone === '13000000000',
|
||||
hasUserWithEmail: async (email: string) => email === 'a@a.com',
|
||||
hasActiveUsers: async () => hasActiveUsers(),
|
||||
}));
|
||||
|
||||
const grantSave = jest.fn(async () => 'finalGrantId');
|
||||
|
|
|
@ -15,9 +15,10 @@ import {
|
|||
generateUserId,
|
||||
findUserByUsernameAndPassword,
|
||||
updateLastSignInAt,
|
||||
insertUser,
|
||||
} from '@/lib/user';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import { hasUser, insertUser, hasActiveUsers } from '@/queries/user';
|
||||
import { hasUser, hasActiveUsers } from '@/queries/user';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
import { AnonymousRouter } from '../types';
|
||||
|
|
|
@ -9,10 +9,6 @@ import { createRequester } from '@/utils/test-utils';
|
|||
|
||||
import sessionSocialRoutes from './social';
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
generateUserId: () => 'user1',
|
||||
updateLastSignInAt: async (...args: unknown[]) => updateUserById(...args),
|
||||
}));
|
||||
jest.mock('@/lib/social', () => ({
|
||||
...jest.requireActual('@/lib/social'),
|
||||
async findSocialRelatedUser() {
|
||||
|
@ -39,14 +35,21 @@ jest.mock('@/lib/social', () => ({
|
|||
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
|
||||
jest.mock('@/queries/user', () => ({
|
||||
findUserById: async () => findUserById(),
|
||||
findUserByIdentity: async () => ({ id: 'id', identities: {} }),
|
||||
insertUser: async (...args: unknown[]) => insertUser(...args),
|
||||
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
||||
hasUserWithIdentity: async (target: string, userId: string) =>
|
||||
target === 'connectorTarget' && userId === 'id',
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
generateUserId: () => 'user1',
|
||||
updateLastSignInAt: async (...args: unknown[]) => updateUserById(...args),
|
||||
insertUser: async (...args: unknown[]) => insertUser(...args),
|
||||
}));
|
||||
|
||||
const getConnectorInstanceByIdHelper = jest.fn(async (connectorId: string) => {
|
||||
const connector = {
|
||||
enabled: connectorId === 'social_enabled',
|
||||
|
|
|
@ -12,11 +12,10 @@ import {
|
|||
getUserInfoByAuthCode,
|
||||
getUserInfoFromInteractionResult,
|
||||
} from '@/lib/social';
|
||||
import { generateUserId, updateLastSignInAt } from '@/lib/user';
|
||||
import { generateUserId, insertUser, updateLastSignInAt } from '@/lib/user';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import {
|
||||
hasUserWithIdentity,
|
||||
insertUser,
|
||||
findUserById,
|
||||
updateUserById,
|
||||
findUserByIdentity,
|
||||
|
|
Loading…
Reference in a new issue