From ea3ad15f5fd8e5669b713d1c73f2d7b67211e991 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sun, 4 Jun 2023 14:45:05 +0800 Subject: [PATCH] feat: add ban user you can see this action in admin-next --- client/shared/model/user.ts | 3 ++ server/admin-next/package.json | 2 +- server/admin-next/src/client/fields.ts | 1 + server/admin-next/src/client/i18n.ts | 6 +++ .../admin-next/src/client/resources/user.tsx | 32 ++++++++++++- server/admin-next/src/server/broker.ts | 2 +- server/admin-next/src/server/router/api.ts | 13 +++++- server/mixins/socketio.mixin.ts | 22 +++++++++ server/package.json | 3 +- .../packages/sdk/src/services/lib/errors.ts | 7 ++- server/services/core/user/user.service.ts | 45 +++++++++++++++++-- 11 files changed, 125 insertions(+), 11 deletions(-) diff --git a/client/shared/model/user.ts b/client/shared/model/user.ts index 87e84674..c1a1f421 100644 --- a/client/shared/model/user.ts +++ b/client/shared/model/user.ts @@ -17,6 +17,7 @@ export interface UserBaseInfo { avatar: string | null; temporary: boolean; emailVerified: boolean; + banned: boolean; extra?: Record; } @@ -51,6 +52,7 @@ export function pickUserBaseInfo(userInfo: UserLoginInfo): UserBaseInfo { 'avatar', 'temporary', 'emailVerified', + 'banned', ]); } @@ -64,6 +66,7 @@ const builtinUserInfo: Record UserBaseInfo> = { avatar: null, temporary: false, emailVerified: false, + banned: false, }), }; diff --git a/server/admin-next/package.json b/server/admin-next/package.json index cfb4ce5b..49dd251c 100644 --- a/server/admin-next/package.json +++ b/server/admin-next/package.json @@ -25,7 +25,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "tailchat-server-sdk": "workspace:^", - "tushan": "^0.2.12", + "tushan": "^0.2.16", "vite-express": "0.8.0" }, "devDependencies": { diff --git a/server/admin-next/src/client/fields.ts b/server/admin-next/src/client/fields.ts index e9d25ca4..9df5cde9 100644 --- a/server/admin-next/src/client/fields.ts +++ b/server/admin-next/src/client/fields.ts @@ -49,6 +49,7 @@ export const userFields = [ createAvatarField('avatar', { preRenderTransform: parseUrlStr, }), + createBooleanField('banned'), createJSONField('settings', { list: { width: 200, diff --git a/server/admin-next/src/client/i18n.ts b/server/admin-next/src/client/i18n.ts index 30fc5b9f..f2a6b017 100644 --- a/server/admin-next/src/client/i18n.ts +++ b/server/admin-next/src/client/i18n.ts @@ -12,6 +12,9 @@ export const i18n: TushanContextProps['i18n'] = { custom: { action: { resetPassword: 'Reset Password', + banUser: 'Ban User', + banUserDesc: + 'Banning a user disconnects the user from the current connection and prevents future logins', }, dashboard: { file: 'File', @@ -76,6 +79,7 @@ export const i18n: TushanContextProps['i18n'] = { temporary: '是否游客', type: '用户类型', settings: '用户设置', + banned: '是否被封禁', createdAt: '创建时间', }, }, @@ -147,6 +151,8 @@ export const i18n: TushanContextProps['i18n'] = { custom: { action: { resetPassword: '重置密码', + banUser: '封禁用户', + banUserDesc: '封禁用户会将用户从当前连接断开并阻止之后的登录操作', }, dashboard: { file: '文件', diff --git a/server/admin-next/src/client/resources/user.tsx b/server/admin-next/src/client/resources/user.tsx index 681cf8e6..4d402224 100644 --- a/server/admin-next/src/client/resources/user.tsx +++ b/server/admin-next/src/client/resources/user.tsx @@ -4,16 +4,19 @@ import { ListTable, Message, Modal, + useRefreshList, useResourceContext, useTranslation, useUpdate, } from 'tushan'; import { userFields } from '../fields'; +import { request } from '../request'; export const UserList: React.FC = React.memo(() => { const { t } = useTranslation(); const [update] = useUpdate(); const resource = useResourceContext(); + const refreshUser = useRefreshList(resource); return ( { detail: true, edit: true, delete: true, + refresh: true, export: true, custom: [ { key: 'resetPassword', label: t('custom.action.resetPassword'), - onClick: (record: any) => { + onClick: (record) => { const { close } = Modal.confirm({ title: t('tushan.common.confirmTitle'), content: t('tushan.common.confirmContent'), - onConfirm: async (e) => { + onConfirm: async () => { try { await update(resource, { 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 ], }} /> diff --git a/server/admin-next/src/server/broker.ts b/server/admin-next/src/server/broker.ts index 961e56d3..12585c5a 100644 --- a/server/admin-next/src/server/broker.ts +++ b/server/admin-next/src/server/broker.ts @@ -10,7 +10,7 @@ export const broker = new TcBroker({ }); broker.start().then(() => { - console.log('Linked to Tailchat network, TRANSPORTER: ', transporter); + console.log('Connnected to Tailchat network, TRANSPORTER: ', transporter); }); export function callBrokerAction( diff --git a/server/admin-next/src/server/router/api.ts b/server/admin-next/src/server/router/api.ts index 9890e95b..e147eed9 100644 --- a/server/admin-next/src/server/router/api.ts +++ b/server/admin-next/src/server/router/api.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; import jwt from 'jsonwebtoken'; -import { callBrokerAction } from '../broker'; +import { broker, callBrokerAction } from '../broker'; import { adminAuth, auth, authSecret } from '../middleware/auth'; import { configRouter } from './config'; import { networkRouter } from './network'; @@ -99,6 +99,17 @@ router.get('/user/count/summary', auth(), async (req, res) => { 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( '/users', auth(), diff --git a/server/mixins/socketio.mixin.ts b/server/mixins/socketio.mixin.ts index e1cd56c4..6ff27766 100644 --- a/server/mixins/socketio.mixin.ts +++ b/server/mixins/socketio.mixin.ts @@ -397,6 +397,28 @@ export const TcSocketIOService = ( }, }, + /** + * 获取userId获取所有的用户的token + */ + getUserSocketToken: { + visibility: 'public', + params: { + userId: 'string', + }, + async handler( + this: TcService, + ctx: TcContext<{ userId: string }> + ): Promise { + 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); + }, + }, + /** * 踢出用户 */ diff --git a/server/package.json b/server/package.json index f5d3d66d..c417ab11 100644 --- a/server/package.json +++ b/server/package.json @@ -7,11 +7,12 @@ "license": "Apache-2.0", "private": true, "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:sdk": "cd packages/sdk && pnpm watch", "dev:plugins": "pnpm run --filter \"./plugins/*\" build:web:watch", "dev:admin": "cd admin && pnpm dev", + "dev:admin-next": "cd admin-next && pnpm dev", "debug": "node --inspect -r ts-node/register ./runner.ts", "build": "ts-node scripts/build.ts", "start:service": "cd dist && tailchat-runner --config moleculer.config.js", diff --git a/server/packages/sdk/src/services/lib/errors.ts b/server/packages/sdk/src/services/lib/errors.ts index 7183847c..a41cfa03 100644 --- a/server/packages/sdk/src/services/lib/errors.ts +++ b/server/packages/sdk/src/services/lib/errors.ts @@ -40,7 +40,12 @@ export class NoPermissionError extends TcError { export class BannedError extends TcError { 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 + ); } } diff --git a/server/services/core/user/user.service.ts b/server/services/core/user/user.service.ts index 35347552..467039bf 100644 --- a/server/services/core/user/user.service.ts +++ b/server/services/core/user/user.service.ts @@ -20,7 +20,7 @@ import { EntityError, db, call, - NoPermissionError, + BannedError, } from 'tailchat-server-sdk'; import { generateRandomNumStr, @@ -56,6 +56,7 @@ class UserService extends TcService { 'avatar', 'type', 'emailVerified', + 'banned', 'extra', 'createdAt', ]); @@ -144,6 +145,12 @@ class UserService extends TcService { token: 'string', }, }); + this.registerAction('banUser', this.banUser, { + params: { + userId: 'string', + }, + visibility: 'public', + }); this.registerAction('whoami', this.whoami); this.registerAction( 'searchUserWithUniqueName', @@ -291,7 +298,7 @@ class UserService extends TcService { } if (user.banned === true) { - throw new NoPermissionError(t('用户被封禁'), 403); + throw new BannedError(t('用户被封禁'), 403); } // Transform user entity (remove password and all protected fields) @@ -646,11 +653,11 @@ class UserService extends TcService { // token 中没有 _id 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); if (user.banned === true) { - throw new NoPermissionError(t('用户被封禁')); + throw new BannedError(t('用户被封禁')); } 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) { return ctx.meta ?? null; }