0
Fork 0
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:
Wang Sijie 2022-06-22 10:22:15 +08:00 committed by GitHub
parent 67613aec7a
commit 9194a6ee54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 106 additions and 16 deletions

View file

@ -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 (

View file

@ -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>

View file

@ -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}`
)}`
);

View file

@ -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';

View file

@ -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}
`

View file

@ -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;

View file

@ -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',

View file

@ -96,6 +96,8 @@ const translation = {
title: '管理面板',
sign_out: '登出',
profile: '账户管理',
admin_user: '管理员',
system_app: '系统应用',
copy: {
pending: '拷贝',
copying: '拷贝中',