mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
feat(console,core): hide admin user (#1182)
* feat(console,core): hide admin user * fix: extract hideAdminUser * test: add tests for hideAdminUser
This commit is contained in:
parent
67613aec7a
commit
9194a6ee54
8 changed files with 106 additions and 16 deletions
|
@ -1,6 +1,7 @@
|
|||
import { Application } from '@logto/schemas';
|
||||
import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
|
@ -15,8 +16,9 @@ const ApplicationName = ({ applicationId, isLink = false }: Props) => {
|
|||
const isAdminConsole = applicationId === adminConsoleApplicationId;
|
||||
|
||||
const { data } = useSWR<Application>(!isAdminConsole && `/api/applications/${applicationId}`);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const name = (isAdminConsole ? 'Admin Console' : data?.name) || '-';
|
||||
const name = (isAdminConsole ? <>Admin Console ({t('system_app')})</> : data?.name) || '-';
|
||||
|
||||
if (isLink && !isAdminConsole) {
|
||||
return (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { User } from '@logto/schemas';
|
||||
import { User, UserRole } from '@logto/schemas';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
@ -20,18 +20,23 @@ const UserName = ({ userId, isLink = false }: Props) => {
|
|||
const isLoading = !data && !error;
|
||||
const name = data?.name || t('users.unnamed');
|
||||
|
||||
const isAdmin = data?.roleNames.includes(UserRole.Admin);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.userName}>
|
||||
{isLink ? (
|
||||
{isLink && !isAdmin ? (
|
||||
<Link to={`/users/${userId}`} target="_blank" className={styles.link}>
|
||||
{name}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{name}</span>
|
||||
<span>
|
||||
{name}
|
||||
{isAdmin && <> ({t('admin_user')})</>}
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.userId}>{userId}</span>
|
||||
</div>
|
||||
|
|
|
@ -36,7 +36,7 @@ const Users = () => {
|
|||
const pageIndex = Number(query.get('page') ?? '1');
|
||||
const keyword = query.get('search') ?? '';
|
||||
const { data, error, mutate } = useSWR<[User[], number], RequestError>(
|
||||
`/api/users?page=${pageIndex}&page_size=${pageSize}${conditionalString(
|
||||
`/api/users?page=${pageIndex}&page_size=${pageSize}&hideAdminUser=true${conditionalString(
|
||||
keyword && `&search=${keyword}`
|
||||
)}`
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Users } from '@logto/schemas';
|
||||
import { UserRole, Users } from '@logto/schemas';
|
||||
import { createMockPool, createMockQueryResult, sql } from 'slonik';
|
||||
|
||||
import { mockUser } from '@/__mocks__';
|
||||
|
@ -280,6 +280,31 @@ describe('user query', () => {
|
|||
await expect(countUsers(search)).resolves.toEqual(dbvalue);
|
||||
});
|
||||
|
||||
it('countUsers with hideAdminUser', async () => {
|
||||
const search = 'foo';
|
||||
const expectSql = sql`
|
||||
select count(*)
|
||||
from ${table}
|
||||
where not (${fields.roleNames}::jsonb?$1)
|
||||
and (${fields.primaryEmail} like $2 or ${fields.primaryPhone} like $3 or ${fields.username} like $4 or ${fields.name} like $5)
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([
|
||||
UserRole.Admin,
|
||||
`%${search}%`,
|
||||
`%${search}%`,
|
||||
`%${search}%`,
|
||||
`%${search}%`,
|
||||
]);
|
||||
|
||||
return createMockQueryResult([dbvalue]);
|
||||
});
|
||||
|
||||
await expect(countUsers(search, true)).resolves.toEqual(dbvalue);
|
||||
});
|
||||
|
||||
it('findUsers', async () => {
|
||||
const search = 'foo';
|
||||
const limit = 100;
|
||||
|
@ -311,6 +336,39 @@ describe('user query', () => {
|
|||
await expect(findUsers(limit, offset, search)).resolves.toEqual([dbvalue]);
|
||||
});
|
||||
|
||||
it('findUsers with hideAdminUser', async () => {
|
||||
const search = 'foo';
|
||||
const limit = 100;
|
||||
const offset = 1;
|
||||
const expectSql = sql`
|
||||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
from ${table}
|
||||
where not (${fields.roleNames}::jsonb?$1)
|
||||
and (${fields.primaryEmail} like $2 or ${fields.primaryPhone} like $3 or ${
|
||||
fields.username
|
||||
} like $4 or ${fields.name} like $5)
|
||||
limit $6
|
||||
offset $7
|
||||
`;
|
||||
|
||||
mockQuery.mockImplementationOnce(async (sql, values) => {
|
||||
expectSqlAssert(sql, expectSql.sql);
|
||||
expect(values).toEqual([
|
||||
UserRole.Admin,
|
||||
`%${search}%`,
|
||||
`%${search}%`,
|
||||
`%${search}%`,
|
||||
`%${search}%`,
|
||||
limit,
|
||||
offset,
|
||||
]);
|
||||
|
||||
return createMockQueryResult([dbvalue]);
|
||||
});
|
||||
|
||||
await expect(findUsers(limit, offset, search, true)).resolves.toEqual([dbvalue]);
|
||||
});
|
||||
|
||||
it('updateUserById', async () => {
|
||||
const username = 'Joe';
|
||||
const id = 'foo';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { User, CreateUser, Users } from '@logto/schemas';
|
||||
import { User, CreateUser, Users, UserRole } from '@logto/schemas';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
import { buildInsertInto } from '@/database/insert-into';
|
||||
|
@ -94,19 +94,37 @@ const buildUserSearchConditionSql = (search: string) => {
|
|||
return sql`${sql.join(conditions, sql` or `)}`;
|
||||
};
|
||||
|
||||
export const countUsers = async (search?: string) =>
|
||||
const buildUserConditions = (search?: string, hideAdminUser?: boolean) => {
|
||||
if (hideAdminUser) {
|
||||
return sql`
|
||||
where not (${fields.roleNames}::jsonb?${UserRole.Admin})
|
||||
${conditionalSql(search, (search) => sql`and (${buildUserSearchConditionSql(search)})`)}
|
||||
`;
|
||||
}
|
||||
|
||||
return sql`
|
||||
${conditionalSql(search, (search) => sql`where ${buildUserSearchConditionSql(search)}`)}
|
||||
`;
|
||||
};
|
||||
|
||||
export const countUsers = async (search?: string, hideAdminUser?: boolean) =>
|
||||
envSet.pool.one<{ count: number }>(sql`
|
||||
select count(*)
|
||||
from ${table}
|
||||
${conditionalSql(search, (search) => sql`where ${buildUserSearchConditionSql(search)}`)}
|
||||
${buildUserConditions(search, hideAdminUser)}
|
||||
`);
|
||||
|
||||
export const findUsers = async (limit: number, offset: number, search?: string) =>
|
||||
export const findUsers = async (
|
||||
limit: number,
|
||||
offset: number,
|
||||
search?: string,
|
||||
hideAdminUser?: boolean
|
||||
) =>
|
||||
envSet.pool.any<User>(
|
||||
sql`
|
||||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
from ${table}
|
||||
${conditionalSql(search, (search) => sql`where ${buildUserSearchConditionSql(search)}`)}
|
||||
${buildUserConditions(search, hideAdminUser)}
|
||||
limit ${limit}
|
||||
offset ${offset}
|
||||
`
|
||||
|
|
|
@ -3,7 +3,7 @@ import { passwordRegEx, usernameRegEx } from '@logto/shared';
|
|||
import { has } from '@silverhand/essentials';
|
||||
import pick from 'lodash.pick';
|
||||
import { InvalidInputError } from 'slonik';
|
||||
import { object, string } from 'zod';
|
||||
import { literal, object, string } from 'zod';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { encryptUserPassword, generateUserId } from '@/lib/user';
|
||||
|
@ -28,16 +28,19 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
|
|||
router.get(
|
||||
'/users',
|
||||
koaPagination(),
|
||||
koaGuard({ query: object({ search: string().optional() }) }),
|
||||
koaGuard({
|
||||
query: object({ search: string().optional(), hideAdminUser: literal('true').optional() }),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { limit, offset } = ctx.pagination;
|
||||
const {
|
||||
query: { search },
|
||||
query: { search, hideAdminUser: _hideAdminUser },
|
||||
} = ctx.guard;
|
||||
|
||||
const hideAdminUser = _hideAdminUser === 'true';
|
||||
const [{ count }, users] = await Promise.all([
|
||||
countUsers(search),
|
||||
findUsers(limit, offset, search),
|
||||
countUsers(search, hideAdminUser),
|
||||
findUsers(limit, offset, search, hideAdminUser),
|
||||
]);
|
||||
|
||||
ctx.pagination.totalCount = count;
|
||||
|
|
|
@ -96,6 +96,8 @@ const translation = {
|
|||
title: 'Admin Console',
|
||||
sign_out: 'Sign out',
|
||||
profile: 'Profile',
|
||||
admin_user: 'Admin',
|
||||
system_app: 'System',
|
||||
copy: {
|
||||
pending: 'Copy',
|
||||
copying: 'Copying',
|
||||
|
|
|
@ -96,6 +96,8 @@ const translation = {
|
|||
title: '管理面板',
|
||||
sign_out: '登出',
|
||||
profile: '账户管理',
|
||||
admin_user: '管理员',
|
||||
system_app: '系统应用',
|
||||
copy: {
|
||||
pending: '拷贝',
|
||||
copying: '拷贝中',
|
||||
|
|
Loading…
Reference in a new issue