From adc1d95c8d03b31b09d28c145298806efb5d6530 Mon Sep 17 00:00:00 2001 From: zhangkaili Date: Fri, 5 Feb 2021 11:34:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=82=80=E8=AF=B7=E6=96=B9?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scenes/Login/Service.js | 115 +++++++++++++ app/utils/uploadFile.js | 2 +- server/api/auth.js | 37 ++-- server/auth/index.js | 2 + server/auth/invitation.js | 190 +++++++++++++++++++++ server/models/User.js | 1 + shared/i18n/locales/zh_CN/translation.json | 133 ++++++++------- 7 files changed, 405 insertions(+), 75 deletions(-) create mode 100644 server/auth/invitation.js diff --git a/app/scenes/Login/Service.js b/app/scenes/Login/Service.js index 0ec374c42745..b988ebf4c040 100644 --- a/app/scenes/Login/Service.js +++ b/app/scenes/Login/Service.js @@ -15,6 +15,7 @@ type Props = { isCreate: boolean, onEmailSuccess: (email: string) => void, onLdapSuccess: (username: string) => void, + onInvSuccess: (username: string) => void, }; type State = { @@ -24,6 +25,10 @@ type State = { ldapId: string, ldapPassword: string, showLdapSignin: boolean, + accountId: string, + accountPwd: string, + invCode: string, + showInvSignin: boolean, }; class Service extends React.Component { @@ -34,6 +39,10 @@ class Service extends React.Component { ldapId: "", ldapPassword: "", showLdapSignin: false, + accountId: "", + accountPwd: "", + invCode: "", + showInvSignin: false, }; handleChangeEmail = (event: SyntheticInputEvent) => { @@ -48,6 +57,18 @@ class Service extends React.Component { this.setState({ ldapPassword: event.target.value }); }; + handleChangeAccountId = (event: SyntheticInputEvent) => { + this.setState({accountId : event.target.value }); + }; + + handleChangePwdForAccount = (event: SyntheticInputEvent) => { + this.setState({ accountPwd: event.target.value }); + }; + + handleChangeCodeForAccount = (event: SyntheticInputEvent) => { + this.setState({ invCode: event.target.value }); + }; + handleSubmitEmail = async (event: SyntheticEvent) => { event.preventDefault(); @@ -98,6 +119,34 @@ class Service extends React.Component { } }; + handleSubmitInvitation = async (event: SyntheticEvent) => { + event.preventDefault(); + + if (this.state.showInvSignin && this.state.accountId) { + this.setState({ isSubmitting: true }); + + try { + // console.log("action:", event.currentTarget.action); + const response = await client.post(event.currentTarget.action, { + username: this.state.accountId, + password: this.state.accountPwd, + invCode: this.state.invCode, + }); + if (response.redirect) { + window.location.href = response.redirect; + } else { + console.log("login success for invitation"); + this.props.onInvSuccess(this.state.accountId); + } + } finally { + console.log("submit failure"); + this.setState({ isSubmitting: false }); + } + } else { + this.setState({ showInvSignin: true }); + } + }; + render() { const { isCreate, id, name, authUrl } = this.props; @@ -194,6 +243,72 @@ class Service extends React.Component { ); } + if (id === "invitation") { + if (isCreate) { + return null; + } + + console.log("id:", id); + console.log("name:", name); + console.log("authUrl:", authUrl); + console.log("invitation type"); + return ( + +
+ {this.state.showInvSignin ? ( + <> + + + + + + Sign In → + + + ) : ( + } fullwidth> + Continue with Invitation + + )} + +
+ ); + } + const icon = id === "slack" ? ( diff --git a/app/utils/uploadFile.js b/app/utils/uploadFile.js index f2dc99ef78ef..bd19df2ff5e3 100644 --- a/app/utils/uploadFile.js +++ b/app/utils/uploadFile.js @@ -48,7 +48,7 @@ export const uploadFile = async ( formData.append("file", file); } - let bucketDir = "wiki" + data.form.key; + let bucketDir = "finance-wiki" + data.form.key; console.log("bucketDir: ", bucketDir); try { diff --git a/server/api/auth.js b/server/api/auth.js index a829edccc097..c2cc5809eda9 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -30,28 +30,33 @@ if (process.env.GOOGLE_CLIENT_ID) { // }); // } -// services.push({ -// id: "ldap", -// name: "LDAP", -// authUrl: "", -// }); - services.push({ - id: "ldap", - name: "LDAP", - authUrl: "", - }, - // { - // id: "email", - // name: "Email", - // authUrl: "", - // } -); + id: "invitation", + name: "Invitation", + authUrl: "", +}); + +// services.push({ +// id: "ldap", +// name: "LDAP", +// authUrl: "", +// }, +// // { +// // id: "email", +// // name: "Email", +// // authUrl: "", +// // } +// ); function filterServices(team) { let output = services; + // 邀请制 if (team) { + output = reject(output, (service) => service.id === "invitation"); + } + + if (team && !team.ldapId) { output = reject(output, (service) => service.id === "ldap"); } if (team && !team.googleId) { diff --git a/server/auth/index.js b/server/auth/index.js index 2b2a16def7da..2a7a9e6d8731 100644 --- a/server/auth/index.js +++ b/server/auth/index.js @@ -13,6 +13,7 @@ import email from "./email"; import google from "./google"; import slack from "./slack"; import ldap from "./ldap"; +import invitation from "./invitation"; const app = new Koa(); app.use(Cors()); @@ -22,6 +23,7 @@ router.use("/", slack.routes()); router.use("/", google.routes()); router.use("/", email.routes()); router.use("/", ldap.routes()); +router.use("/", invitation.routes()); router.get("/redirect", auth(), async (ctx) => { const user = ctx.state.user; diff --git a/server/auth/invitation.js b/server/auth/invitation.js new file mode 100644 index 000000000000..7e5fff1e8555 --- /dev/null +++ b/server/auth/invitation.js @@ -0,0 +1,190 @@ +// @flow + +import Router from "koa-router"; +import methodOverride from "../middlewares/methodOverride"; +import validation from "../middlewares/validation"; +import addHours from "date-fns/add_hours"; +import {getCookieDomain} from "../utils/domains"; +import {v4 as uuidv4} from "uuid"; +import {Event, Team, User} from "../models"; +import Sequelize from "sequelize"; +import invariant from "invariant"; +import auth from "../middlewares/authentication"; +import crypto from "crypto"; +import addMonths from "date-fns/add_months"; + +const router = new Router(); +router.use(methodOverride()); +router.use(validation()); + +router.post("invitation", async (ctx) => { + + const {username, password, invCode} = ctx.body; + + ctx.assertPresent(username, "accountID is required!"); + ctx.assertPresent(password, "accountPwd is required!"); + ctx.assertPresent(invCode, "invCode is required!"); + + const team = await Team.findByPk(process.env.TEAM_ID); + + const teamUrl = process.env.TEAM_REDIRECT_URL; + + // ps: ctx.redirect 并不发生实质性的跳转 + // let authInfo = []; + + try { + // 校验邀请码 + const isValidation = invCodeValidation(invCode); + if (false === isValidation) { + console.log("邀请码错误!"); + ctx.body = { + redirect: `${teamUrl}?notice=invcode_validation`, + message: "邀请码错误,请重新尝试登陆", + success: false, + }; + // 跳转到错误页面 + return; + } + + } catch (e) { + console.log("用户名或密码不正确!"); + ctx.body = { + redirect: `${teamUrl}?notice=ldap_validation`, + message: "认证失败,请重新尝试登陆", + success: false, + }; + // 跳转到错误页面 + return; + } + + console.log("ppp:", md5Crypto(password)); + const user = await User.findOne({ + where: { + username: username, + password: md5Crypto(password), + }, + }); + + if (user) { + + if (!team) { + ctx.redirect(`/?notice=auth-error`); + return; + } + + console.log("service:", user.service); + + if (user.service && "innerInv" !== user.service) { + ctx.body = { + redirect: `${teamUrl}/auth/${user.service}`, + }; + return; + } + console.log("认证成功", `${teamUrl}/auth/invitation.callback?accountId=${username}&permit=${md5Crypto(password)}`); + ctx.body = { + redirect: `${teamUrl}/auth/invitation.callback?accountId=${username}&permit=${md5Crypto(password)}` + }; + return; + } + ctx.body = { + redirect: `${teamUrl}/auth/invitation.callback?accountId=${username}&permit=${md5Crypto(password)}` + }; +}); + +router.get("invitation.callback", auth({required: false}), async (ctx) => { + const {accountId, permit} = ctx.request.query; + const state = Math.random().toString(36).substring(7); + ctx.cookies.set("state", state, { + httpOnly: false, + expires: addHours(new Date(), 1), + domain: getCookieDomain(ctx.request.hostname), + }); + + let userPrimary = uuidv4(); + let team, isFirstUser; + try { + [team, isFirstUser] = await Team.findOrCreate({ + where: { + name: "finance", + }, + defaults: { + name: "finance", + avatarUrl: "https://a.slack-edge.com/80588/img/avatars-teams/ava_0017-88.png", + }, + }); + } catch (err) { + if (err instanceof Sequelize.UniqueConstraintError) { + ctx.redirect(`/?notice=auth-error`); + ctx.status(307); + return; + } + } + invariant(team, "Team must exist"); + + try { + const [user, isFirstSignin] = await User.findOrCreate({ + where: { + username: accountId, + password: permit, + }, + defaults: { + id: userPrimary, + email: "", + username: accountId, + name: accountId, + password: permit, + isAdmin: isFirstUser, + serviceId: userPrimary, + createdAt: new Date(), + updatedAt: new Date(), + lastActiveAt: new Date(), + lastActiveIp: ctx.request.ip, + lastSignedInAt: new Date(), + lastSignedInIp: ctx.request.ip, + teamId: process.env.TEAM_ID, + avatarUrl: "https://sorel-lookbook.com/wp-content/themes/sorel-microsite-theme/library/img/default-person.png", + service: "innerInv", + language: "zh_CN", + }, + }); + + if (isFirstUser) { + await team.provisionFirstCollection(userPrimary); + await team.provisionSubdomain(team.domain); + } + + console.log("222222"); + ctx.signIn(user, team, "innerInv", isFirstSignin); + } catch (err) { + if (err instanceof Sequelize.UniqueConstraintError) { + const exists = await User.findOne({ + where: { + service: "innerInv", + email: "", + teamId: team.id, + }, + }); + + if (exists) { + ctx.redirect(`${team.url}?notice=email-auth-required`); + } else { + ctx.redirect(`${team.url}?notice=auth-error`); + } + } + throw err; + } + +}); + +function md5Crypto(str) { + const hash = crypto.createHash('md5') + hash.update(str) + return hash.digest('hex'); +} + +function invCodeValidation(userInvCode) { + return userInvCode === process.env.INVITATION_CODE; + +} + +export default router; diff --git a/server/models/User.js b/server/models/User.js index 5a8e043aa713..a94331eb475b 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -24,6 +24,7 @@ const User = sequelize.define( email: { type: DataTypes.STRING }, username: { type: DataTypes.STRING }, name: DataTypes.STRING, + password: { type: DataTypes.STRING }, avatarUrl: { type: DataTypes.STRING, allowNull: true }, isAdmin: DataTypes.BOOLEAN, service: { type: DataTypes.STRING, allowNull: true }, diff --git a/shared/i18n/locales/zh_CN/translation.json b/shared/i18n/locales/zh_CN/translation.json index 20c09cdb0ef5..a1d38cc0eaf1 100644 --- a/shared/i18n/locales/zh_CN/translation.json +++ b/shared/i18n/locales/zh_CN/translation.json @@ -5,14 +5,15 @@ "You": "你", "Trash": "回收站", "Archive": "归档", - "Drafts": "草稿", + "Drafts": "草稿箱", "Templates": "文档模板", "Deleted Collection": "删除文档集", + "Submenu": "子菜单", "New": "新", "Only visible to you": "只对您可见", "Draft": "草稿", "Template": "模板", - "New doc": "新文档", + "New doc": "新建文档", "deleted": "已删除", "archived": "已归档", "created": "已创建", @@ -22,16 +23,15 @@ "Never viewed": "未被浏览", "Viewed": "已浏览", "in": "在", - "More options": "更多选项", "Insert column after": "向后插入列", - "Insert column before": "向前插入列", + "Insert column before": "在左侧插入列", "Insert row after": "上方插入行", "Insert row before": "下方插入行", "Align center": "居中对齐", "Align left": "左对齐", "Align right": "右对齐", "Bulleted list": "无序列表", - "Todo list": "待办事项列表", + "Todo list": "待办事项", "Code block": "代码块", "Copied to clipboard": "已复制到剪切板", "Code": "代码", @@ -52,11 +52,11 @@ "Image": "图片", "Sorry, an error occurred uploading the image": "抱歉,上传图片时发生错误", "Info": "信息", - "Info notice": "信息提示", + "Info notice": "提示信息", "Link": "链接", "Link copied to clipboard": "链接已经复制到剪贴板", "Highlight": "高亮", - "Type '/' to insert": "输入/以插入", + "Type '/' to insert": "输入“/”以插入", "Keep typing to filter": "继续输入以过滤", "No results": "没有找到结果", "Open link": "打开链接", @@ -76,73 +76,85 @@ "Warning": "警告", "Warning notice": "警告信息", "Icon": "图标", + "Show menu": "显示菜单", + "Choose icon": "选择图标", "Loading": "加载中", "Search": "搜索", "Outline is available in your language {{optionLabel}}, would you like to change?": "Outline 当前支持 {{optionLabel}},您想要更改吗?", - "Change Language": "更改界面语言", + "Change Language": "语言设置", "Dismiss": "忽略", "Keyboard shortcuts": "快捷键", "Expand": "展开", "Collapse": "折叠", "New collection": "新建文档集", "Collections": "文档集", - "Untitled": "无标题文档", + "Untitled": "无标题", "Home": "主页", - "Starred": "已加星标", - "Invite people…": "邀请其他人…", + "Starred": "收藏夹", + "Settings": "设置", "Invite people": "邀请其他人", "Create a collection": "创建文档集", "Return to App": "返回应用", + "Account": "账号", "Profile": "基本资料", "Notifications": "通知", - "API Tokens": "API Tokens", - "Details": "详细信息", + "API Tokens": "API 令牌", + "Team": "团队", + "Details": "详情", "Security": "安全性", "People": "用户", "Groups": "用户组", - "Share Links": "已共享链接", + "Share Links": "分享链接", "Export Data": "导出数据", "Integrations": "集成", "Installation": "安装", - "Settings": "设置", - "API documentation": "API 文档", - "Changelog": "更新日志", - "Send us feedback": "发送反馈", - "Report a bug": "反馈 Bug", - "Appearance": "界面外观", + "Resize sidebar": "调整侧边栏", + "Unstar": "取消收藏", + "Star": "加入收藏", + "Appearance": "界面风格", "System": "系统信息", "Light": "浅色主题", "Dark": "深色主题", + "API documentation": "API 文档", + "Changelog": "更新日志", + "Send us feedback": "反馈给我们", + "Report a bug": "提交 Bug", "Log out": "退出登录", - "Collection permissions": "文档集权限", + "Show path to document": "显示文档路径", + "Path to document": "文件路径", + "Group member options": "小组成员选项", + "Members": "成员", + "Remove": "移除", + "Collection": "文档集", "New document": "新建文档", "Import document": "导入文档", "Edit": "编辑", "Permissions": "权限", "Export": "导出", "Delete": "删除", + "Collection permissions": "文档集权限", "Edit collection": "编辑文档集", "Delete collection": "删除文档集", "Export collection": "导出文档集", + "Show sort menu": "显示排序菜单", "Sort in sidebar": "调整侧边栏的显示顺序", "Alphabetical sort": "按字母顺序排序", "Manual sort": "手动排序", "Document duplicated": "文档已复制", - "Document archived": "文件已封存", + "Document archived": "文件已归档", "Document restored": "文档已恢复", "Document unpublished": "未发布的文档", + "Document options": "文档选项", "Restore": "恢复", "Choose a collection": "选择一个文档集", "Unpin": "取消置顶", "Pin to collection": "置顶文档集", - "Unstar": "取消星标", - "Star": "加星标", - "Share link": "共享链接", + "Share link": "分享链接", "Enable embeds": "启用嵌入", "Disable embeds": "禁用嵌入", "New nested document": "新的嵌套文档", "Create template": "创建模板", - "Duplicate": "复制项目", + "Duplicate": "复制", "Unpublish": "取消发布", "Move": "移动", "History": "历史记录", @@ -152,46 +164,51 @@ "Share document": "共享文档", "Edit group": "编辑群组", "Delete group": "刪除群組", - "Members": "成员", + "Group options": "分组选项", + "Member options": "成员选项", "collection": "文档集", + "New child document": "新的嵌套文档", "New document in <1>{{collectionName}}": "在<1>{{collectionName}}中创建新文档", "New template": "新建模板", "Link copied": "链接已复制", + "Revision options": "修订选项", "Restore version": "恢复此版本", "Copy link": "复制链接", - "Share link revoked": "已撤销分享链接", + "Share link revoked": "已撤销共享链接", "Share link copied": "分享链接已复制", + "Share options": "分享选项", "Go to document": "转到文档", "Revoke link": "撤消链接", - "By {{ author }}": "作者 {{ author }}", + "By {{ author }}": "创建人 {{ author }}", "Are you sure you want to make {{ userName }} an admin? Admins can modify team and billing information.": "您确定要设置 {{ userName }} 为管理员吗?管理员可以修改团队和帐单信息。", "Are you sure you want to make {{ userName }} a member?": "您确定要设置 {{ userName }} 为成员吗?", "Are you sure you want to suspend this account? Suspended users will be prevented from logging in.": "您确定要冻结此帐户吗?被冻结的用户将无法登录。", + "User options": "用户选项", "Make {{ userName }} a member…": "将 {{ userName }} 设为成员…", "Make {{ userName }} an admin…": "将 {{ userName }} 设为管理员…", "Revoke invite": "撤消邀请", "Activate account": "激活帐号", "Suspend account": "冻结账号", "Documents": "文档", - "The document archive is empty at the moment.": "目前没有被归档的文档", + "The document archive is empty at the moment.": "尚未归档的文档", "Search in collection": "在文档集中搜索", - "<0>{{collectionName}} doesn’t contain any documents yet.": "<0>{{collectionName}}目前还没有任何文档", + "<0>{{collectionName}} doesn’t contain any documents yet.": "<0>{{collectionName}}为空文档集", "Get started by creating a new one!": "从创建一个新文档集开始!", "Create a document": "创建文档", "Manage members": "管理成员", "Pinned": "已置顶", "Recently updated": "最近更新", "Recently published": "最近发布", - "Least recently updated": "最旧未更新", - "A–Z": "字母序", + "Least recently updated": "最近最少更新", + "A–Z": "A-Z", "The collection was updated": "文档集已更新", "You can edit the name and other details at any time, however doing so often might confuse your team mates.": "您可以随时编辑姓名和其他详细信息,但是经常这样做可能会使您的队友感到困惑。", "Name": "名称", "Description": "说明", "More details about this collection…": "有关此文档集的更多详细信息…", "Alphabetical": "按字母顺序排列", - "Private collection": "私密文档集", - "A private collection will only be visible to invited team members.": "私密文档集仅对受邀团队成员可见。", + "Private collection": "私有文档集", + "A private collection will only be visible to invited team members.": "私有文档集仅对受邀团队成员可见。", "Saving": "保存中", "Save": "保存", "{{ groupName }} was added to the collection": "{{ groupName }} 已添加到文档集", @@ -199,8 +216,8 @@ "Can’t find the group you’re looking for?": "找不到您正在寻找的组?", "Create a group": "创建组", "Search by group name": "按组名搜索", - "Search groups": "搜索分组", - "No groups matching your search": "没有符合您搜索的组", + "Search groups": "搜索组", + "No groups matching your search": "没有匹配信息", "No groups left to add": "没有可添加的组", "Add": "添加", "{{ userName }} was added to the collection": "{{ userName }} 已添加到文档集", @@ -212,21 +229,21 @@ "No people left to add": "没有人可以添加", "Read only": "只读", "Read & Edit": "阅读和编辑", - "Remove": "移除", "Active <1> ago": "<1>前活跃", "Never signed in": "从未登录", - "Invited": "已获邀", + "Invited": "已邀请", "Admin": "管理员", - "Collections are for grouping your knowledge base. They work best when organized around a topic or internal team — Product or Engineering for example.": "文档集用于对文档进行分组。建议使用文档集管理同一主题或固定团队(比如产品团队或工程师团队)的文档。", + "Collections are for grouping your knowledge base. They work best when organized around a topic or internal team — Product or Engineering for example.": "文档集用于对文档进行分组。建议使用文档集管理同一主题或固定团队(比如产品团队或研发团队)的文档。", "Creating": "正在创建", "Create": "创建", "Recently viewed": "最近浏览", "Created by me": "由我创建", - "Hide contents": "隐藏目录", - "Show contents": "显示目录", + "Search documents": "搜索文档", + "Hide contents": "隐藏内容", + "Show contents": "显示内容", "Archived": "已归档", - "Anyone with the link <1>can view this document": "拥有此链接的任何人<1>都能查看此内容", - "Share": "共享", + "Anyone with the link <1>can view this document": "拥有此链接<1>的任何人都能查看此内容", + "Share": "分享", "Save Draft": "存为草稿", "Done Editing": "编辑完成", "Edit {{noun}}": "编辑 {{noun}}", @@ -235,17 +252,17 @@ "Publish document": "发布文档", "Publishing": "发布中", "Are you sure you want to delete the <2>{{documentTitle}} template?": "您确定要删除<2>{{documentTitle}} 模板吗?", - "Are you sure about that? Deleting the <2>{{documentTitle}} document will delete all of its history and any nested documents.": "您确定要删除吗?删除<2>{{documentTitle}}文档将删除其所有历史记录以及所有的嵌套文件。", + "Are you sure about that? Deleting the <2>{{documentTitle}} document will delete all of its history and any nested documents.": "您确定要删除吗?删除<2>{{documentTitle}}文档将删除其所有历史记录以及所有的相关文件。", "If you’d like the option of referencing or restoring the {{noun}} in the future, consider archiving it instead.": "如果您将来希望引用或还原{{noun}},请考虑将其存档。", "Deleting": "正在删除", - "I’m sure – Delete": "我确定–删除", + "I’m sure – Delete": "确定– 删除", "Archiving": "正在归档", "No documents found for your filters.": "没有找到相关文档。", "You’ve not got any drafts at the moment.": "您目前还没有任何草稿。", - "Not found": "没有找到", + "Not found": "未找到", "We were unable to find the page you’re looking for. Go to the <2>homepage?": "我们找不到您要查找的页面。转到<2>主页?", "Offline": "离线", - "We were unable to load the document while offline.": "离线时我们无法加载文档。", + "We were unable to load the document while offline.": "离线状态无法加载文档。", "Your account has been suspended": "您的账户已被停用", "A team admin (<1>{{suspendedContactEmail}}) has suspended your account. To re-activate your account, please reach out to them directly.": "团队管理员(<1>{{suspendedContactEmail}} )已暂停您的帐户。要重新激活您的帐户,请直接与他们联系。", "{{userName}} was added to the group": "{{userName}} 已添加到群组", @@ -255,16 +272,16 @@ "Could not remove user": "无法删除用户", "Add people": "添加人员", "This group has no members.": "这个群组没有任何成员", - "Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and there’s Markdown too.": "Outline 旨在快速且易于使用。您常用的键盘快捷键都可使用,还支持 Markdown 语法。", + "Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and there’s Markdown too.": "Outline 旨在快速且易于使用。您常用的快捷键都可使用,并且支持 Markdown 语法。", "Navigation": "导航", "New document in current collection": "在当前文档集中创建新文档", "Edit current document": "编辑当前文档", "Move current document": "移动当前文档", "Jump to search": "跳转到搜索", - "Jump to dashboard": "跳转到仪表板", + "Jump to dashboard": "跳转到主页", "Table of contents": "目录", "Toggle sidebar": "切换侧边栏", - "Open this guide": "打开本指南", + "Open this guide": "打开指南", "Editor": "编辑器", "Save and exit document edit mode": "保存并退出文档编辑模式", "Publish and exit document edit mode": "发布并退出文档编辑模式", @@ -286,20 +303,20 @@ "Use the <1>{{meta}}+K shortcut to search from anywhere in your knowledge base": "在知识库的任何位置,按下 <1>{{meta}}+K 快捷键就可以启动搜索", "No documents found for your search filters. <1>Create a new document?": "找不到相关文档。<1>创建一个新文档?", "Clear filters": "清除筛选", - "Profile saved": "配置文件已保存", - "Profile picture updated": "个人图片已更新", - "Unable to upload new profile picture": "不能上传个人资料照片", + "Profile saved": "配置已保存", + "Profile picture updated": "头像已更新", + "Unable to upload new profile picture": "无法上传个人图片", "Photo": "图片", "Upload": "上传", "Full name": "全名", "Language": "语言", - "Please note that translations are currently in early access.<1>Community contributions are accepted though our <4>translation portal": "请注意,目前翻译是抢先版本。<1>我们通过<4>翻译门户网站接受社区贡献", + "Please note that translations are currently in early access.<1>Community contributions are accepted though our <4>translation portal": "请注意,目前翻译是待反馈版本。<1>我们通过<4>翻译门户网站接受社区贡献", "Delete Account": "删除帐户", - "You may delete your account at any time, note that this is unrecoverable": "您可以随时删除您的帐户,请注意,这是无法恢复的", + "You may delete your account at any time, note that this is unrecoverable": "您可以随时删除您的帐户,请注意删除后将无法恢复该账号", "Delete account": "删除账户", - "You’ve not starred any documents yet.": "您尚未标记文档。", + "You’ve not starred any documents yet.": "您尚未标记任何文档。", "There are no templates just yet. You can create templates to help your team create consistent and accurate documentation.": "目前还没有模板。您可以创建模板来帮助您的团队创建风格一致和准确的文档。", - "Trash is empty at the moment.": "回收站目前是空的。", + "Trash is empty at the moment.": "回收站为空。", "You joined": "您已加入", "Joined": "已加入", "{{ time }} ago.": "{{ time }} 之前。",