feat: add ban user

you can see this action in admin-next
pull/90/head
moonrailgun 2 years ago
parent 75021144c3
commit ea3ad15f5f

@ -17,6 +17,7 @@ export interface UserBaseInfo {
avatar: string | null; avatar: string | null;
temporary: boolean; temporary: boolean;
emailVerified: boolean; emailVerified: boolean;
banned: boolean;
extra?: Record<string, unknown>; extra?: Record<string, unknown>;
} }
@ -51,6 +52,7 @@ export function pickUserBaseInfo(userInfo: UserLoginInfo): UserBaseInfo {
'avatar', 'avatar',
'temporary', 'temporary',
'emailVerified', 'emailVerified',
'banned',
]); ]);
} }
@ -64,6 +66,7 @@ const builtinUserInfo: Record<string, () => UserBaseInfo> = {
avatar: null, avatar: null,
temporary: false, temporary: false,
emailVerified: false, emailVerified: false,
banned: false,
}), }),
}; };

@ -25,7 +25,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"tailchat-server-sdk": "workspace:^", "tailchat-server-sdk": "workspace:^",
"tushan": "^0.2.12", "tushan": "^0.2.16",
"vite-express": "0.8.0" "vite-express": "0.8.0"
}, },
"devDependencies": { "devDependencies": {

@ -49,6 +49,7 @@ export const userFields = [
createAvatarField('avatar', { createAvatarField('avatar', {
preRenderTransform: parseUrlStr, preRenderTransform: parseUrlStr,
}), }),
createBooleanField('banned'),
createJSONField('settings', { createJSONField('settings', {
list: { list: {
width: 200, width: 200,

@ -12,6 +12,9 @@ export const i18n: TushanContextProps['i18n'] = {
custom: { custom: {
action: { action: {
resetPassword: 'Reset Password', resetPassword: 'Reset Password',
banUser: 'Ban User',
banUserDesc:
'Banning a user disconnects the user from the current connection and prevents future logins',
}, },
dashboard: { dashboard: {
file: 'File', file: 'File',
@ -76,6 +79,7 @@ export const i18n: TushanContextProps['i18n'] = {
temporary: '是否游客', temporary: '是否游客',
type: '用户类型', type: '用户类型',
settings: '用户设置', settings: '用户设置',
banned: '是否被封禁',
createdAt: '创建时间', createdAt: '创建时间',
}, },
}, },
@ -147,6 +151,8 @@ export const i18n: TushanContextProps['i18n'] = {
custom: { custom: {
action: { action: {
resetPassword: '重置密码', resetPassword: '重置密码',
banUser: '封禁用户',
banUserDesc: '封禁用户会将用户从当前连接断开并阻止之后的登录操作',
}, },
dashboard: { dashboard: {
file: '文件', file: '文件',

@ -4,16 +4,19 @@ import {
ListTable, ListTable,
Message, Message,
Modal, Modal,
useRefreshList,
useResourceContext, useResourceContext,
useTranslation, useTranslation,
useUpdate, useUpdate,
} from 'tushan'; } from 'tushan';
import { userFields } from '../fields'; import { userFields } from '../fields';
import { request } from '../request';
export const UserList: React.FC = React.memo(() => { export const UserList: React.FC = React.memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const [update] = useUpdate(); const [update] = useUpdate();
const resource = useResourceContext(); const resource = useResourceContext();
const refreshUser = useRefreshList(resource);
return ( return (
<ListTable <ListTable
@ -28,16 +31,17 @@ export const UserList: React.FC = React.memo(() => {
detail: true, detail: true,
edit: true, edit: true,
delete: true, delete: true,
refresh: true,
export: true, export: true,
custom: [ custom: [
{ {
key: 'resetPassword', key: 'resetPassword',
label: t('custom.action.resetPassword'), label: t('custom.action.resetPassword'),
onClick: (record: any) => { onClick: (record) => {
const { close } = Modal.confirm({ const { close } = Modal.confirm({
title: t('tushan.common.confirmTitle'), title: t('tushan.common.confirmTitle'),
content: t('tushan.common.confirmContent'), content: t('tushan.common.confirmContent'),
onConfirm: async (e) => { onConfirm: async () => {
try { try {
await update(resource, { await update(resource, {
id: record.id, id: record.id,
@ -56,6 +60,30 @@ export const UserList: React.FC = React.memo(() => {
}); });
}, },
}, },
{
key: 'banUser',
label: t('custom.action.banUser'),
onClick: (record) => {
const { close } = Modal.confirm({
title: t('tushan.common.confirmTitle'),
content: t('custom.action.banUserDesc'),
onConfirm: async () => {
try {
await request.post('/user/ban', {
userId: record.id,
});
Message.success(t('tushan.common.success'));
refreshUser();
close();
} catch (err) {
console.error(err);
Message.error(String(err));
}
},
});
},
},
// TODO: unban
], ],
}} }}
/> />

@ -10,7 +10,7 @@ export const broker = new TcBroker({
}); });
broker.start().then(() => { broker.start().then(() => {
console.log('Linked to Tailchat network, TRANSPORTER: ', transporter); console.log('Connnected to Tailchat network, TRANSPORTER: ', transporter);
}); });
export function callBrokerAction<T>( export function callBrokerAction<T>(

@ -1,6 +1,6 @@
import { Router } from 'express'; import { Router } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { callBrokerAction } from '../broker'; import { broker, callBrokerAction } from '../broker';
import { adminAuth, auth, authSecret } from '../middleware/auth'; import { adminAuth, auth, authSecret } from '../middleware/auth';
import { configRouter } from './config'; import { configRouter } from './config';
import { networkRouter } from './network'; import { networkRouter } from './network';
@ -99,6 +99,17 @@ router.get('/user/count/summary', auth(), async (req, res) => {
res.json({ summary }); res.json({ summary });
}); });
router.post('/user/ban', auth(), async (req, res) => {
const { userId } = req.body;
const ret = await broker.call('user.banUser', {
userId,
});
res.json({
ret,
});
});
router.use( router.use(
'/users', '/users',
auth(), auth(),

@ -397,6 +397,28 @@ export const TcSocketIOService = (
}, },
}, },
/**
* userIdtoken
*/
getUserSocketToken: {
visibility: 'public',
params: {
userId: 'string',
},
async handler(
this: TcService,
ctx: TcContext<{ userId: string }>
): Promise<string[]> {
const userId = ctx.params.userId;
const io: SocketServer = this.io;
const remoteSockets = await io
.in(buildUserRoomId(userId))
.fetchSockets();
return remoteSockets.map((remoteSocket) => remoteSocket.data.token);
},
},
/** /**
* *
*/ */

@ -7,11 +7,12 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "concurrently --kill-others npm:dev:main npm:dev:sdk npm:dev:plugins npm:dev:admin", "dev": "concurrently --kill-others npm:dev:main npm:dev:sdk npm:dev:plugins npm:dev:admin-next",
"dev:main": "ts-node ./runner.ts", "dev:main": "ts-node ./runner.ts",
"dev:sdk": "cd packages/sdk && pnpm watch", "dev:sdk": "cd packages/sdk && pnpm watch",
"dev:plugins": "pnpm run --filter \"./plugins/*\" build:web:watch", "dev:plugins": "pnpm run --filter \"./plugins/*\" build:web:watch",
"dev:admin": "cd admin && pnpm dev", "dev:admin": "cd admin && pnpm dev",
"dev:admin-next": "cd admin-next && pnpm dev",
"debug": "node --inspect -r ts-node/register ./runner.ts", "debug": "node --inspect -r ts-node/register ./runner.ts",
"build": "ts-node scripts/build.ts", "build": "ts-node scripts/build.ts",
"start:service": "cd dist && tailchat-runner --config moleculer.config.js", "start:service": "cd dist && tailchat-runner --config moleculer.config.js",

@ -40,7 +40,12 @@ export class NoPermissionError extends TcError {
export class BannedError extends TcError { export class BannedError extends TcError {
constructor(message?: string, code?: number, type?: string, data?: unknown) { constructor(message?: string, code?: number, type?: string, data?: unknown) {
super(message ?? 'You has been banned', code ?? 403, type, data); super(
message ?? 'You has been banned',
code ?? 403,
type ?? 'banned',
data
);
} }
} }

@ -20,7 +20,7 @@ import {
EntityError, EntityError,
db, db,
call, call,
NoPermissionError, BannedError,
} from 'tailchat-server-sdk'; } from 'tailchat-server-sdk';
import { import {
generateRandomNumStr, generateRandomNumStr,
@ -56,6 +56,7 @@ class UserService extends TcService {
'avatar', 'avatar',
'type', 'type',
'emailVerified', 'emailVerified',
'banned',
'extra', 'extra',
'createdAt', 'createdAt',
]); ]);
@ -144,6 +145,12 @@ class UserService extends TcService {
token: 'string', token: 'string',
}, },
}); });
this.registerAction('banUser', this.banUser, {
params: {
userId: 'string',
},
visibility: 'public',
});
this.registerAction('whoami', this.whoami); this.registerAction('whoami', this.whoami);
this.registerAction( this.registerAction(
'searchUserWithUniqueName', 'searchUserWithUniqueName',
@ -291,7 +298,7 @@ class UserService extends TcService {
} }
if (user.banned === true) { if (user.banned === true) {
throw new NoPermissionError(t('用户被封禁'), 403); throw new BannedError(t('用户被封禁'), 403);
} }
// Transform user entity (remove password and all protected fields) // Transform user entity (remove password and all protected fields)
@ -646,11 +653,11 @@ class UserService extends TcService {
// token 中没有 _id // token 中没有 _id
throw new EntityError(t('Token 内容不正确')); throw new EntityError(t('Token 内容不正确'));
} }
const doc = await this.getById(decoded._id); const doc = await this.adapter.model.findById(decoded._id);
const user: User = await this.transformDocuments(ctx, {}, doc); const user: User = await this.transformDocuments(ctx, {}, doc);
if (user.banned === true) { if (user.banned === true) {
throw new NoPermissionError(t('用户被封禁')); throw new BannedError(t('用户被封禁'));
} }
const json = await this.transformEntity(user, true, ctx.meta.token); const json = await this.transformEntity(user, true, ctx.meta.token);
@ -670,6 +677,36 @@ class UserService extends TcService {
} }
} }
/**
*
*/
async banUser(
ctx: TcContext<{
userId: string;
}>
) {
const { userId } = ctx.params;
await this.adapter.model.updateOne(
{
_id: userId,
},
{
banned: true,
}
);
this.cleanUserInfoCache(userId);
const tokens = await ctx.call('gateway.getUserSocketToken', {
userId,
});
if (Array.isArray(tokens)) {
tokens.map((token) => this.cleanActionCache('resolveToken', [token]));
}
await ctx.call('gateway.tickUser', {
userId,
});
}
async whoami(ctx: TcContext) { async whoami(ctx: TcContext) {
return ctx.meta ?? null; return ctx.meta ?? null;
} }

Loading…
Cancel
Save