diff --git a/packages/core/client/src/locale/en_US.json b/packages/core/client/src/locale/en_US.json index cd7a9dfcd..a91fe2f9e 100644 --- a/packages/core/client/src/locale/en_US.json +++ b/packages/core/client/src/locale/en_US.json @@ -830,6 +830,6 @@ "Designer properties": "Designer properties", "Load": "Load", "Dump": "Dump", - "Chang on Parent":"Chang on Parent", - "Please enter search content":"Please enter search content" + "Chang on Parent": "Chang on Parent", + "Please enter search content": "Please enter search content" } diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index e5e88f5fe..d1d909ca0 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -934,5 +934,5 @@ "Sorry, the page you visited does not exist.": "对不起,您访问的页面不存在。", "Back Home": "回到主页", "Chang on Parent": "选择父级", - "Please enter search content":"请输入查询内容" + "Please enter search content": "请输入查询内容" } diff --git a/packages/plugins/@tachybase/plugin-acl/src/server/server.ts b/packages/plugins/@tachybase/plugin-acl/src/server/server.ts index 387208c11..92c87e2f2 100644 --- a/packages/plugins/@tachybase/plugin-acl/src/server/server.ts +++ b/packages/plugins/@tachybase/plugin-acl/src/server/server.ts @@ -412,8 +412,13 @@ export class PluginACL extends Plugin { if (plugin.getName() !== 'users') { return; } + + // 为默认角色添加权限组 const User = this.db.getCollection('users'); await User.repository.update({ + filter: { + id: 1, + }, values: { roles: ['root', 'admin', 'member'], }, diff --git a/packages/plugins/@tachybase/plugin-share/.npmignore b/packages/plugins/@tachybase/plugin-share/.npmignore new file mode 100644 index 000000000..65f5e8779 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/.npmignore @@ -0,0 +1,2 @@ +/node_modules +/src diff --git a/packages/plugins/@tachybase/plugin-share/README.md b/packages/plugins/@tachybase/plugin-share/README.md new file mode 100644 index 000000000..20d21a191 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/README.md @@ -0,0 +1,14 @@ +# share + +English | [中文](./README.zh-CN.md) + +## Install + +``` +pnpm pm add @tachybase/plugin-share +pnpm pm enable @tachybase/plugin-share +``` + +## Usage + +TODO diff --git a/packages/plugins/@tachybase/plugin-share/README.zh-CN.md b/packages/plugins/@tachybase/plugin-share/README.zh-CN.md new file mode 100644 index 000000000..fa6cc02bb --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/README.zh-CN.md @@ -0,0 +1,14 @@ +# share + +[English](./README.md) | 中文 + +## 安装激活 + +``` +pnpm pm add @tachybase/plugin-share +pnpm pm enable @tachybase/plugin-share +``` + +## 使用方法 + +TODO diff --git a/packages/plugins/@tachybase/plugin-share/client.d.ts b/packages/plugins/@tachybase/plugin-share/client.d.ts new file mode 100644 index 000000000..6c459cbac --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/client.d.ts @@ -0,0 +1,2 @@ +export * from './dist/client'; +export { default } from './dist/client'; diff --git a/packages/plugins/@tachybase/plugin-share/client.js b/packages/plugins/@tachybase/plugin-share/client.js new file mode 100644 index 000000000..b6e3be70e --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/client.js @@ -0,0 +1 @@ +module.exports = require('./dist/client/index.js'); diff --git a/packages/plugins/@tachybase/plugin-share/package.json b/packages/plugins/@tachybase/plugin-share/package.json new file mode 100644 index 000000000..e907c8bf2 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tachybase/plugin-share", + "displayName": "Share Externally", + "version": "0.21.73", + "description": "Share pages with external users.", + "keywords": [ + "Users & permissions" + ], + "license": "Apache-2.0", + "main": "dist/server/index.js", + "dependencies": {}, + "devDependencies": {}, + "peerDependencies": { + "@tachybase/actions": "workspace:*", + "@tachybase/auth": "workspace:*", + "@tachybase/client": "workspace:*", + "@tachybase/database": "workspace:*", + "@tachybase/server": "workspace:*", + "@tachybase/test": "workspace:*" + }, + "description.zh-CN": "与外部用户分享页面。", + "displayName.zh-CN": "对外分享" +} diff --git a/packages/plugins/@tachybase/plugin-share/server.d.ts b/packages/plugins/@tachybase/plugin-share/server.d.ts new file mode 100644 index 000000000..c41081ddc --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/server.d.ts @@ -0,0 +1,2 @@ +export * from './dist/server'; +export { default } from './dist/server'; diff --git a/packages/plugins/@tachybase/plugin-share/server.js b/packages/plugins/@tachybase/plugin-share/server.js new file mode 100644 index 000000000..972842039 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/server.js @@ -0,0 +1 @@ +module.exports = require('./dist/server/index.js'); diff --git a/packages/plugins/@tachybase/plugin-share/src/client/index.tsx b/packages/plugins/@tachybase/plugin-share/src/client/index.tsx new file mode 100644 index 000000000..0b0ef0dc3 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/src/client/index.tsx @@ -0,0 +1,5 @@ +import { Plugin } from '@tachybase/client'; + +export class PluginShareClient extends Plugin {} + +export default PluginShareClient; diff --git a/packages/plugins/@tachybase/plugin-share/src/constants.ts b/packages/plugins/@tachybase/plugin-share/src/constants.ts new file mode 100644 index 000000000..5af8a5680 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/src/constants.ts @@ -0,0 +1,3 @@ +import { name } from '../package.json'; + +export const NAMESPACE = name; diff --git a/packages/plugins/@tachybase/plugin-share/src/index.ts b/packages/plugins/@tachybase/plugin-share/src/index.ts new file mode 100644 index 000000000..7e74612df --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/src/index.ts @@ -0,0 +1,2 @@ +export * from './server'; +export { default } from './server'; diff --git a/packages/plugins/@tachybase/plugin-share/src/locale/en-US.json b/packages/plugins/@tachybase/plugin-share/src/locale/en-US.json new file mode 100644 index 000000000..000378e13 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/src/locale/en-US.json @@ -0,0 +1,6 @@ +{ + "Guest": "Guest", + "Guest user not found.": "Guest user not found.", + "Not allowed to modify login information of guest user.": "Not allowed to modify login information of guest user.", + "Unauthorized": "Unauthorized" +} diff --git a/packages/plugins/@tachybase/plugin-share/src/locale/zh-CN.json b/packages/plugins/@tachybase/plugin-share/src/locale/zh-CN.json new file mode 100644 index 000000000..672905246 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/src/locale/zh-CN.json @@ -0,0 +1,6 @@ +{ + "Guest": "访客", + "Guest user not found.": "访客用户未找到。", + "Not allowed to modify login information of guest user.": "不允许修改访客用户的登录信息。", + "Unauthorized": "未授权" +} diff --git a/packages/plugins/@tachybase/plugin-share/src/server/actions/users.ts b/packages/plugins/@tachybase/plugin-share/src/server/actions/users.ts new file mode 100644 index 000000000..b065990ea --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/src/server/actions/users.ts @@ -0,0 +1,52 @@ +import { Context, Next } from '@tachybase/actions'; + +import { NAMESPACE } from '../../constants'; + +export async function update(ctx: Context, next: Next) { + const { values } = ctx.action.params; + const targetUserId = Number(ctx.request.url.split('filterByTk=')[1]); + const guestUser = await ctx.db.getRepository('users').findOne({ + filter: { + username: 'guest', + }, + raw: true, + }); + + if (!guestUser) { + ctx.throw(401, ctx.t('Guest user not found.', { ns: NAMESPACE })); + } + + // 只拦截 update guest 用户的请求 + if (targetUserId === guestUser.id) { + if ( + (values.username && values.username !== guestUser.username) || + (values.email && values.email !== guestUser.email) || + values.password + ) { + ctx.throw(401, ctx.t('Not allowed to modify login information of guest user.', { ns: NAMESPACE })); + } + } + + const UserRepo = ctx.db.getRepository('users'); + const result = await UserRepo.update({ + filterByTk: targetUserId, + values, + }); + ctx.body = result; + await next(); +} + +export async function updateProfile(ctx: Context, next: Next) { + const { values } = ctx.action.params; + const { currentUser } = ctx.state; + if (!currentUser || currentUser.username === 'guest' || currentUser.email === 'guest@tachybase.com') { + ctx.throw(401, ctx.t('Not allowed to modify login information of guest user.', { ns: NAMESPACE })); + } + const UserRepo = ctx.db.getRepository('users'); + const result = await UserRepo.update({ + filterByTk: currentUser.id, + values, + }); + ctx.body = result; + await next(); +} diff --git a/packages/plugins/@tachybase/plugin-share/src/server/collections/.gitkeep b/packages/plugins/@tachybase/plugin-share/src/server/collections/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugins/@tachybase/plugin-share/src/server/index.ts b/packages/plugins/@tachybase/plugin-share/src/server/index.ts new file mode 100644 index 000000000..b68aea57f --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/src/server/index.ts @@ -0,0 +1 @@ +export { default } from './plugin'; diff --git a/packages/plugins/@tachybase/plugin-share/src/server/middlewares/ban-guest-action.ts b/packages/plugins/@tachybase/plugin-share/src/server/middlewares/ban-guest-action.ts new file mode 100644 index 000000000..d1cc99a7a --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/src/server/middlewares/ban-guest-action.ts @@ -0,0 +1,35 @@ +import { Context } from '@tachybase/actions'; + +import { NAMESPACE } from '../../constants'; + +// 禁止访客进行任何鉴权操作,除了 check 和 signOut +function banGuestActionMiddleware() { + return async (ctx: Context, next) => { + // 注意由于已经注入访客 token, 还需要判断请求体里面用户是否为 guest + const reqBody = ctx.request.body; + if ( + ctx.action.resourceName === 'auth' && + !['check', 'signOut'].includes(ctx.action.actionName) && + ctx.auth.user && + (reqBody.account === 'guest' || reqBody.account === 'guest@tachybase.com') + ) { + const { username, email } = ctx.auth.user; + if (username === 'guest' || email === 'guest@tachybase.com') { + ctx.withoutDataWrapping = true; + ctx.status = 401; + ctx.body = { + errors: [ + { + message: ctx.t('Unauthorized', { ns: NAMESPACE }), + }, + ], + }; + return; + } + } + + await next(); + }; +} + +export { banGuestActionMiddleware }; diff --git a/packages/plugins/@tachybase/plugin-share/src/server/plugin.ts b/packages/plugins/@tachybase/plugin-share/src/server/plugin.ts new file mode 100644 index 000000000..0804f5f9a --- /dev/null +++ b/packages/plugins/@tachybase/plugin-share/src/server/plugin.ts @@ -0,0 +1,130 @@ +import { Context } from '@tachybase/actions'; +import { BaseAuth } from '@tachybase/auth'; +import { Plugin } from '@tachybase/server'; + +import * as actions from './actions/users'; +import { banGuestActionMiddleware } from './middlewares/ban-guest-action'; + +export class PluginShareServer extends Plugin { + async beforeLoad() { + for (const [key, action] of Object.entries(actions)) { + this.app.resourcer.registerActionHandler(`users:${key}`, action); + } + + const banGuestAction = banGuestActionMiddleware(); + + this.app.use( + async (ctx: Context, next) => { + try { + await banGuestAction(ctx, next); + } catch (error) { + ctx.logger.error(error); + } + }, + { after: 'restApi', group: 'after' }, + ); + + this.app.acl.addFixedParams('users', 'destroy', () => { + return { + filter: { + 'username.$ne': 'guest', + }, + }; + }); + } + + async load() { + BaseAuth.prototype.check = async function () { + let token = this.ctx.getBearerToken(); + if (!token) { + // 注入访客用户 token, TODO: 也许有更好的方法来拦截 BaseAuth 的逻辑 + // TODO: 访客模式启用判断, 访客 token 有效期设置, 带指定参数才可访客登录,前端导航栏头像适配访客 + const user = await this.userRepository.findOne({ + filter: { + username: 'guest', + }, + raw: true, + }); + if (!user) { + this.ctx.logger.error('guest mode enabled, but no guest user in database', { method: 'check' }); + return null; + } + token = this.jwt.sign({ + userId: user.id, + }); + } + try { + const { userId, roleName } = await this.jwt.decode(token); + + if (roleName) { + this.ctx.headers['x-role'] = roleName; + } + + const cache = this.ctx.cache; + return await cache.wrap(this.getCacheKey(userId), () => + this.userRepository.findOne({ + filter: { + id: userId, + }, + raw: true, + }), + ); + } catch (err) { + this.ctx.logger.error(err, { method: 'check' }); + return null; + } + }; + } + + getInstallingData(options: any = {}) { + const { + guestEmail = 'guest@tachybase.com', + guestPassword = 'N0_PAS5W0RD', + guestNickname = 'Guest', + guestUsername = 'guest', + } = options.users || options?.cliArgs?.[0] || {}; + return { guestEmail, guestPassword, guestNickname, guestUsername }; + } + + async install(options) { + const { guestNickname, guestPassword, guestEmail, guestUsername } = this.getInstallingData(options); + const User = this.db.getCollection('users'); + if (await User.repository.findOne({ filter: { email: guestEmail } })) { + return; + } + + await User.repository.create({ + values: { + email: guestEmail, + password: guestPassword, + nickname: guestNickname, + username: guestUsername, + }, + }); + + const roles = this.db.getCollection('roles'); + await roles.repository.createMany({ + records: [ + { + name: 'guest', + title: '{{t("Guest")}}', + allowConfigure: false, + allowNewMenu: false, + snippets: ['!ui.*', '!pm', '!pm.*'], + }, + ], + }); + + await User.repository.update({ + filter: { + username: 'guest', + }, + values: { + roles: ['guest'], + }, + forceUpdate: true, + }); + } +} + +export default PluginShareServer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b488e5b9..6d5a14842 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3645,6 +3645,27 @@ importers: specifier: ^3.1.0 version: 3.2.0(antd@5.19.1)(react-dom@18.3.1)(react@18.3.1) + packages/plugins/@tachybase/plugin-share: + dependencies: + '@tachybase/actions': + specifier: workspace:* + version: link:../../../core/actions + '@tachybase/auth': + specifier: workspace:* + version: link:../../../core/auth + '@tachybase/client': + specifier: workspace:* + version: link:../../../core/client + '@tachybase/database': + specifier: workspace:* + version: link:../../../core/database + '@tachybase/server': + specifier: workspace:* + version: link:../../../core/server + '@tachybase/test': + specifier: workspace:* + version: link:../../../core/test + packages/plugins/@tachybase/plugin-sms-auth: dependencies: '@tachybase/actions':