feat: invitation link (#3979)
* feat: invitation link schema and apis * feat: add invitation link * feat: member status: active, leave, forbidden * fix: expires show hours and minutes * feat: invalid invitation link hint * fix: typo * chore: fix typo & i18n * fix * pref: fe * feat: add ttl index for 30-day-clean-up
This commit is contained in:
@@ -24,7 +24,10 @@ export enum TeamErrEnum {
|
||||
cannotModifyRootOrg = 'cannotModifyRootOrg',
|
||||
cannotDeleteNonEmptyOrg = 'cannotDeleteNonEmptyOrg',
|
||||
cannotDeleteDefaultGroup = 'cannotDeleteDefaultGroup',
|
||||
userNotActive = 'userNotActive'
|
||||
userNotActive = 'userNotActive',
|
||||
invitationLinkInvalid = 'invitationLinkInvalid',
|
||||
youHaveBeenInTheTeam = 'youHaveBeenInTheTeam',
|
||||
tooManyInvitations = 'tooManyInvitations'
|
||||
}
|
||||
|
||||
const teamErr = [
|
||||
@@ -112,6 +115,18 @@ const teamErr = [
|
||||
{
|
||||
statusText: TeamErrEnum.cannotDeleteNonEmptyOrg,
|
||||
message: i18nT('common:code_error.team_error.cannot_delete_non_empty_org')
|
||||
},
|
||||
{
|
||||
statusText: TeamErrEnum.invitationLinkInvalid,
|
||||
message: i18nT('common:code_error.team_error.invitation_link_invalid')
|
||||
},
|
||||
{
|
||||
statusText: TeamErrEnum.youHaveBeenInTheTeam,
|
||||
message: i18nT('common:code_error.team_error.you_have_been_in_the_team')
|
||||
},
|
||||
{
|
||||
statusText: TeamErrEnum.tooManyInvitations,
|
||||
message: i18nT('common:code_error.team_error.too_many_invitations')
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -14,29 +14,28 @@ export const TeamMemberRoleMap = {
|
||||
};
|
||||
|
||||
export enum TeamMemberStatusEnum {
|
||||
waiting = 'waiting',
|
||||
active = 'active',
|
||||
reject = 'reject',
|
||||
leave = 'leave'
|
||||
leave = 'leave',
|
||||
forbidden = 'forbidden'
|
||||
}
|
||||
|
||||
export const TeamMemberStatusMap = {
|
||||
[TeamMemberStatusEnum.waiting]: {
|
||||
label: 'user.team.member.waiting',
|
||||
color: 'orange.600'
|
||||
},
|
||||
[TeamMemberStatusEnum.active]: {
|
||||
label: 'user.team.member.active',
|
||||
color: 'green.600'
|
||||
},
|
||||
[TeamMemberStatusEnum.reject]: {
|
||||
label: 'user.team.member.reject',
|
||||
color: 'red.600'
|
||||
},
|
||||
[TeamMemberStatusEnum.leave]: {
|
||||
label: 'user.team.member.leave',
|
||||
color: 'red.600'
|
||||
},
|
||||
[TeamMemberStatusEnum.forbidden]: {
|
||||
label: 'user.team.member.forbidden',
|
||||
color: 'red.600'
|
||||
}
|
||||
};
|
||||
|
||||
export const notLeaveStatus = { $ne: TeamMemberStatusEnum.leave };
|
||||
export const notLeaveStatus = {
|
||||
$not: {
|
||||
$in: [TeamMemberStatusEnum.leave, TeamMemberStatusEnum.forbidden]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,11 +40,6 @@ export type UpdateInviteProps = {
|
||||
status: TeamMemberSchema['status'];
|
||||
};
|
||||
|
||||
export type UpdateStatusProps = {
|
||||
tmbId: string;
|
||||
status: TeamMemberSchema['status'];
|
||||
};
|
||||
|
||||
export type InviteMemberResponse = Record<
|
||||
'invite' | 'inValid' | 'inTeam',
|
||||
{ username: string; userId: string }[]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const MaxInvitationLinksAmount = 10;
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isForbidden({ expires, forbidden }: { expires: Date; forbidden?: boolean }) {
|
||||
return forbidden || new Date(expires) < new Date();
|
||||
}
|
||||
54
packages/service/support/user/team/invitationLink/schema.ts
Normal file
54
packages/service/support/user/team/invitationLink/schema.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
TeamCollectionName,
|
||||
TeamMemberCollectionName
|
||||
} from '@fastgpt/global/support/user/team/constant';
|
||||
import { connectionMongo, getMongoModel } from '../../../../common/mongo';
|
||||
import { InvitationSchemaType } from './type';
|
||||
import addDays from 'date-fns/esm/fp/addDays/index.js';
|
||||
const { Schema } = connectionMongo;
|
||||
|
||||
export const InvitationCollectionName = 'team_invitation_links';
|
||||
|
||||
const InvitationSchema = new Schema({
|
||||
teamId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: TeamCollectionName,
|
||||
required: true
|
||||
},
|
||||
usedTimesLimit: {
|
||||
type: Number
|
||||
},
|
||||
forbidden: {
|
||||
type: Boolean
|
||||
},
|
||||
expires: {
|
||||
type: Date
|
||||
},
|
||||
description: {
|
||||
type: String
|
||||
},
|
||||
members: {
|
||||
type: [String],
|
||||
default: []
|
||||
}
|
||||
});
|
||||
|
||||
InvitationSchema.virtual('team', {
|
||||
ref: TeamCollectionName,
|
||||
localField: 'teamId',
|
||||
foreignField: '_id',
|
||||
justOne: true
|
||||
});
|
||||
|
||||
InvitationSchema.index({ expires: 1 }, { expireAfterSeconds: 30 * 24 * 60 * 60 });
|
||||
|
||||
try {
|
||||
InvitationSchema.index({ teamId: 1 }, { background: true });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
export const MongoInvitationLink = getMongoModel<InvitationSchemaType>(
|
||||
InvitationCollectionName,
|
||||
InvitationSchema
|
||||
);
|
||||
37
packages/service/support/user/team/invitationLink/type.ts
Normal file
37
packages/service/support/user/team/invitationLink/type.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { TeamMemberSchema } from '@fastgpt/global/support/user/team/type';
|
||||
|
||||
export type InvitationSchemaType = {
|
||||
_id: string;
|
||||
teamId: string;
|
||||
usedTimesLimit?: number;
|
||||
forbidden?: boolean;
|
||||
expires: Date;
|
||||
description: string;
|
||||
members: string[];
|
||||
};
|
||||
|
||||
export type InvitationType = Omit<InvitationSchemaType, 'members'> & {
|
||||
members: {
|
||||
tmbId: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type InvitationLinkExpiresType = '30m' | '7d' | '1y';
|
||||
|
||||
export type InvitationLinkCreateType = {
|
||||
description: string;
|
||||
expires: InvitationLinkExpiresType;
|
||||
usedTimesLimit: number;
|
||||
};
|
||||
export type InvitationLinkUpdateType = Partial<
|
||||
Omit<InvitationSchemaType, 'members' | 'teamId' | '_id'>
|
||||
> & {
|
||||
linkId: string;
|
||||
};
|
||||
|
||||
export type InvitationInfoType = InvitationSchemaType & {
|
||||
teamAvatar: string;
|
||||
teamName: string;
|
||||
};
|
||||
@@ -10,15 +10,7 @@ import { Box, Flex } from '@chakra-ui/react';
|
||||
* @param [groupId] - group id to make the key unique
|
||||
* @returns
|
||||
*/
|
||||
function AvatarGroup({
|
||||
avatars,
|
||||
max = 3,
|
||||
groupId
|
||||
}: {
|
||||
max?: number;
|
||||
avatars: string[];
|
||||
groupId?: string;
|
||||
}) {
|
||||
function AvatarGroup({ avatars, max = 3 }: { max?: number; avatars: string[] }) {
|
||||
return (
|
||||
<Flex position="relative">
|
||||
{avatars.slice(0, max).map((avatar, index) => (
|
||||
|
||||
@@ -61,6 +61,8 @@ export const iconPaths = {
|
||||
'common/leftArrowLight': () => import('./icons/common/leftArrowLight.svg'),
|
||||
'common/line': () => import('./icons/common/line.svg'),
|
||||
'common/lineChange': () => import('./icons/common/lineChange.svg'),
|
||||
'common/lineStop': () => import('./icons/common/lineStop.svg'),
|
||||
'common/link': () => import('./icons/common/link.svg'),
|
||||
'common/linkBlue': () => import('./icons/common/linkBlue.svg'),
|
||||
'common/list': () => import('./icons/common/list.svg'),
|
||||
'common/loading': () => import('./icons/common/loading.svg'),
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_17994_4)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.98584 2.23348C5.80108 2.23348 3.21932 4.81524 3.21932 8C3.21932 11.1848 5.80108 13.7665 8.98584 13.7665C12.1706 13.7665 14.7524 11.1848 14.7524 8C14.7524 4.81524 12.1706 2.23348 8.98584 2.23348ZM1.93787 8C1.93787 4.10751 5.09335 0.952026 8.98584 0.952026C12.8783 0.952026 16.0338 4.10751 16.0338 8C16.0338 11.8925 12.8783 15.048 8.98584 15.048C5.09335 15.048 1.93787 11.8925 1.93787 8ZM6.42294 6.07782C6.42294 5.72396 6.7098 5.4371 7.06366 5.4371H10.908C11.2619 5.4371 11.5487 5.72396 11.5487 6.07782V9.92217C11.5487 10.276 11.2619 10.5629 10.908 10.5629H7.06366C6.7098 10.5629 6.42294 10.276 6.42294 9.92217V6.07782ZM7.70439 6.71855V9.28145H10.2673V6.71855H7.70439Z" fill="#485264"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_17994_4">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.98584)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1013 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.98608 2.34313C10.5482 0.78103 13.0808 0.78103 14.6429 2.34313C16.205 3.90522 16.205 6.43788 14.6429 7.99998L13.7001 8.94279C13.4398 9.20314 13.0177 9.20314 12.7573 8.94279C12.497 8.68244 12.497 8.26033 12.7573 7.99998L13.7001 7.05717C14.7415 6.01577 14.7415 4.32733 13.7001 3.28594C12.6587 2.24454 10.9703 2.24454 9.92889 3.28594L8.98608 4.22875C8.72573 4.48909 8.30362 4.48909 8.04327 4.22875C7.78292 3.9684 7.78292 3.54629 8.04327 3.28594L8.98608 2.34313ZM11.7908 5.19523C12.0512 5.45558 12.0512 5.87769 11.7908 6.13804L7.12415 10.8047C6.8638 11.0651 6.44169 11.0651 6.18134 10.8047C5.92099 10.5444 5.92099 10.1222 6.18134 9.8619L10.848 5.19523C11.1084 4.93488 11.5305 4.93488 11.7908 5.19523ZM5.21484 7.05717C5.47519 7.31752 5.47519 7.73963 5.21484 7.99998L4.27204 8.94279C3.23064 9.98419 3.23064 11.6726 4.27204 12.714C5.31343 13.7554 7.00187 13.7554 8.04327 12.714L8.98608 11.7712C9.24643 11.5109 9.66854 11.5109 9.92889 11.7712C10.1892 12.0316 10.1892 12.4537 9.92889 12.714L8.98608 13.6568C7.42398 15.2189 4.89132 15.2189 3.32923 13.6568C1.76713 12.0947 1.76713 9.56208 3.32923 7.99998L4.27204 7.05717C4.53239 6.79682 4.9545 6.79682 5.21484 7.05717Z" fill="#485264"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -35,5 +35,27 @@
|
||||
"user_team_invite_member": "Invite members",
|
||||
"user_team_leave_team": "Leave the team",
|
||||
"user_team_leave_team_failed": "Failure to leave the team",
|
||||
"waiting": "To be accepted"
|
||||
"waiting": "To be accepted",
|
||||
"invitation_link_list": "Invitation link list",
|
||||
"create_invitation_link": "Create Invitation Link",
|
||||
"invitation_link_description": "Link description",
|
||||
"30mins": "30 Minutes",
|
||||
"7days": "7 Days",
|
||||
"1year": "1 Year",
|
||||
"unlimited": "Unlimited",
|
||||
"1person": "1 person",
|
||||
"expires": "Expiration",
|
||||
"used_times_limit": "Limit",
|
||||
"invited": "Invited",
|
||||
"has_forbidden": "Forbidden",
|
||||
"forbidden": "Forbidden",
|
||||
"copy_link": "Copy link",
|
||||
"handle_invitation": "Handle Invitation",
|
||||
"ignore": "Ignore",
|
||||
"forbid_success": "Forbid success",
|
||||
"forbid_hint": "After forbidden, this invitation link will become invalid. This action is irreversible. Are you sure you want to deactivate?",
|
||||
"confirm_forbidden": "Confirm forbidden",
|
||||
"invitation_link_auto_clean_hint": "Expired links will be automatically cleaned up after 30 days",
|
||||
"has_invited": "Invited",
|
||||
"invitation_link_has_been_invalid": "The invitation link has expired. Please contact the team administrator"
|
||||
}
|
||||
|
||||
@@ -100,6 +100,9 @@
|
||||
"code_error.team_error.un_auth": "Unauthorized to Operate This Team",
|
||||
"code_error.team_error.user_not_active": "The user did not accept or has left the team",
|
||||
"code_error.team_error.website_sync_not_enough": "The free version cannot be synchronized with the web site ~",
|
||||
"code_error.team_error.invitation_link_invalid": "Invitation link is invalid",
|
||||
"code_error.team_error.you_have_been_in_the_team": "You are already in this team",
|
||||
"code_error.team_error.too_many_invitations": "You have reached the maximum number of active invitation links, please clean up some links first",
|
||||
"code_error.token_error_code.403": "Invalid Login Status, Please Re-login",
|
||||
"code_error.user_error.balance_not_enough": "Insufficient Account Balance",
|
||||
"code_error.user_error.bin_visitor_guest": "You Are Currently a Guest, Unauthorized to Operate",
|
||||
|
||||
@@ -51,5 +51,27 @@
|
||||
"confirm_delete_from_org": "确认将 {{username}} 移出部门?",
|
||||
"search_org": "搜索部门",
|
||||
"notification_recieve": "团队通知接收",
|
||||
"set_name_avatar": "团队头像 & 团队名"
|
||||
"set_name_avatar": "团队头像 & 团队名",
|
||||
"invitation_link_list": "链接列表",
|
||||
"create_invitation_link": "创建邀请链接",
|
||||
"invitation_link_description": "链接描述",
|
||||
"30mins": "30分钟",
|
||||
"7days": "7天",
|
||||
"1year": "1年",
|
||||
"unlimited": "无限制",
|
||||
"1person": "1人",
|
||||
"expires": "有效期",
|
||||
"used_times_limit": "有效人数",
|
||||
"invited": "已邀请",
|
||||
"has_forbidden": "已失效",
|
||||
"forbidden": "停用",
|
||||
"copy_link": "复制链接",
|
||||
"handle_invitation": "处理团队邀请",
|
||||
"ignore": "忽略",
|
||||
"forbid_success": "停用成功",
|
||||
"forbid_hint": "停用后,该邀请链接将失效。 该操作不可撤销,是否确认停用?",
|
||||
"confirm_forbidden": "确认停用",
|
||||
"invitation_link_auto_clean_hint": "已失效链接将在30天后自动清理",
|
||||
"has_invited": "已邀请",
|
||||
"invitation_link_has_been_invalid": "邀请链接已失效,请联系团队管理员"
|
||||
}
|
||||
|
||||
@@ -104,6 +104,9 @@
|
||||
"code_error.team_error.un_auth": "无权操作该团队",
|
||||
"code_error.team_error.user_not_active": "用户未接受或已离开团队",
|
||||
"code_error.team_error.website_sync_not_enough": "免费版无法使用Web站点同步~",
|
||||
"code_error.team_error.invitation_link_invalid": "邀请链接无效",
|
||||
"code_error.team_error.you_have_been_in_the_team": "你已经在该团队中",
|
||||
"code_error.team_error.too_many_invitations": "您的有效邀请链接数已达上限,请先清理链接",
|
||||
"code_error.token_error_code.403": "登录状态无效,请重新登录",
|
||||
"code_error.user_error.balance_not_enough": "账号余额不足~",
|
||||
"code_error.user_error.bin_visitor_guest": "您当前身份为游客,无权操作",
|
||||
|
||||
@@ -35,5 +35,27 @@
|
||||
"user_team_invite_member": "邀請成員",
|
||||
"user_team_leave_team": "離開團隊",
|
||||
"user_team_leave_team_failed": "離開團隊失敗",
|
||||
"waiting": "待接受"
|
||||
"waiting": "待接受",
|
||||
"invitation_link_list": "連結列表",
|
||||
"create_invitation_link": "建立邀請連結",
|
||||
"invitation_link_description": "連結描述",
|
||||
"30mins": "30分鐘",
|
||||
"7days": "7天",
|
||||
"1year": "1年",
|
||||
"unlimited": "無限制",
|
||||
"1person": "1人",
|
||||
"expires": "有效期",
|
||||
"used_times_limit": "有效人數",
|
||||
"invited": "已邀請",
|
||||
"has_forbidden": "已失效",
|
||||
"forbidden": "停用",
|
||||
"copy_link": "複製連結",
|
||||
"handle_invitation": "處理團隊邀請",
|
||||
"ignore": "忽略",
|
||||
"forbid_success": "停用成功",
|
||||
"forbid_hint": "停用後,該邀請連結將失效。 該操作不可撤銷,是否確認停用?",
|
||||
"confirm_forbidden": "確認停用",
|
||||
"invitation_link_auto_clean_hint": "已失效連結將在30天後自動清理",
|
||||
"has_invited": "已邀請",
|
||||
"invitation_link_has_been_invalid": "邀請連結已失效,請聯繫團隊管理員"
|
||||
}
|
||||
|
||||
@@ -99,6 +99,9 @@
|
||||
"code_error.team_error.un_auth": "無權操作此團隊",
|
||||
"code_error.team_error.user_not_active": "使用者未接受或已離開團隊",
|
||||
"code_error.team_error.website_sync_not_enough": "免費版無法使用Web站點同步~",
|
||||
"code_error.team_error.invitation_link_invalid": "邀請連結無效",
|
||||
"code_error.team_error.you_have_been_in_the_team": "你已經在該團隊中",
|
||||
"code_error.team_error.too_many_invitations": "您的有效邀請連結數已達上限,請先清理連結",
|
||||
"code_error.token_error_code.403": "登入狀態無效,請重新登入",
|
||||
"code_error.user_error.balance_not_enough": "帳戶餘額不足",
|
||||
"code_error.user_error.bin_visitor_guest": "您目前身份為訪客,無權操作",
|
||||
|
||||
Reference in New Issue
Block a user