Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
a4db03a3
authored
May 14, 2025
by
Archer
Committed by
GitHub
May 14, 2025
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: session id (#4817)
* feat: session id * feat: Add default index
parent
cba8f773
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
248 additions
and
75 deletions
+248
-75
docSite/content/zh-cn/docs/development/upgrading/499.md
+21
-0
env.d.ts
+1
-1
packages/service/common/redis/cache.ts
+11
-8
packages/service/common/redis/index.ts
+15
-6
packages/service/common/redis/type.d.ts
+1
-1
packages/service/core/ai/utils.ts
+2
-2
packages/service/support/permission/controller.ts
+3
-46
packages/service/support/user/session.ts
+179
-0
projects/app/.env.template
+2
-2
projects/app/src/pages/api/support/user/account/loginByPassword.ts
+13
-9
No files found.
docSite/content/zh-cn/docs/development/upgrading/499.md
0 → 100644
View file @
a4db03a3
---
title
:
'
V4.9.9(进行中)'
description
:
'
FastGPT
V4.9.9
更新说明'
icon
:
'
upgrade'
draft
:
false
toc
:
true
weight
:
791
---
## 🚀 新增内容
1.
切换 SessionId 来替代 JWT 实现登录鉴权,可控制最大登录客户端数量。
## ⚙️ 优化
## 🐛 修复
env.d.ts
View file @
a4db03a3
...
@@ -4,7 +4,6 @@ declare global {
...
@@ -4,7 +4,6 @@ declare global {
LOG_DEPTH
:
string
;
LOG_DEPTH
:
string
;
DEFAULT_ROOT_PSW
:
string
;
DEFAULT_ROOT_PSW
:
string
;
DB_MAX_LINK
:
string
;
DB_MAX_LINK
:
string
;
TOKEN_KEY
:
string
;
FILE_TOKEN_KEY
:
string
;
FILE_TOKEN_KEY
:
string
;
ROOT_KEY
:
string
;
ROOT_KEY
:
string
;
OPENAI_BASE_URL
:
string
;
OPENAI_BASE_URL
:
string
;
...
@@ -37,6 +36,7 @@ declare global {
...
@@ -37,6 +36,7 @@ declare global {
CONFIG_JSON_PATH
?:
string
;
CONFIG_JSON_PATH
?:
string
;
PASSWORD_LOGIN_LOCK_SECONDS
?:
string
;
PASSWORD_LOGIN_LOCK_SECONDS
?:
string
;
PASSWORD_EXPIRED_MONTH
?:
string
;
PASSWORD_EXPIRED_MONTH
?:
string
;
MAX_LOGIN_SESSION
?:
string
;
}
}
}
}
}
}
...
...
packages/service/common/redis/cache.ts
View file @
a4db03a3
import
{
getGlobalRedisC
acheC
onnection
}
from
'./index'
;
import
{
getGlobalRedisConnection
}
from
'./index'
;
import
{
addLog
}
from
'../system/log'
;
import
{
addLog
}
from
'../system/log'
;
import
{
retryFn
}
from
'@fastgpt/global/common/system/utils'
;
import
{
retryFn
}
from
'@fastgpt/global/common/system/utils'
;
const
redisPrefix
=
'cache:'
;
const
getCacheKey
=
(
key
:
string
)
=>
`
${
redisPrefix
}${
key
}
`
;
export
enum
CacheKeyEnum
{
export
enum
CacheKeyEnum
{
team_vector_count
=
'team_vector_count'
team_vector_count
=
'team_vector_count'
}
}
...
@@ -13,12 +16,12 @@ export const setRedisCache = async (
...
@@ -13,12 +16,12 @@ export const setRedisCache = async (
)
=>
{
)
=>
{
return
await
retryFn
(
async
()
=>
{
return
await
retryFn
(
async
()
=>
{
try
{
try
{
const
redis
=
getGlobalRedisC
acheC
onnection
();
const
redis
=
getGlobalRedisConnection
();
if
(
expireSeconds
)
{
if
(
expireSeconds
)
{
await
redis
.
set
(
key
,
data
,
'EX'
,
expireSeconds
);
await
redis
.
set
(
getCacheKey
(
key
)
,
data
,
'EX'
,
expireSeconds
);
}
else
{
}
else
{
await
redis
.
set
(
key
,
data
);
await
redis
.
set
(
getCacheKey
(
key
)
,
data
);
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
addLog
.
error
(
'Set cache error:'
,
error
);
addLog
.
error
(
'Set cache error:'
,
error
);
...
@@ -28,11 +31,11 @@ export const setRedisCache = async (
...
@@ -28,11 +31,11 @@ export const setRedisCache = async (
};
};
export
const
getRedisCache
=
async
(
key
:
string
)
=>
{
export
const
getRedisCache
=
async
(
key
:
string
)
=>
{
const
redis
=
getGlobalRedisC
acheC
onnection
();
const
redis
=
getGlobalRedisConnection
();
return
await
retryFn
(()
=>
redis
.
get
(
key
));
return
await
retryFn
(()
=>
redis
.
get
(
getCacheKey
(
key
)
));
};
};
export
const
delRedisCache
=
async
(
key
:
string
)
=>
{
export
const
delRedisCache
=
async
(
key
:
string
)
=>
{
const
redis
=
getGlobalRedisC
acheC
onnection
();
const
redis
=
getGlobalRedisConnection
();
await
retryFn
(()
=>
redis
.
del
(
key
));
await
retryFn
(()
=>
redis
.
del
(
getCacheKey
(
key
)
));
};
};
packages/service/common/redis/index.ts
View file @
a4db03a3
...
@@ -27,17 +27,26 @@ export const newWorkerRedisConnection = () => {
...
@@ -27,17 +27,26 @@ export const newWorkerRedisConnection = () => {
return
redis
;
return
redis
;
};
};
export
const
getGlobalRedisCacheConnection
=
()
=>
{
export
const
FASTGPT_REDIS_PREFIX
=
'fastgpt:'
;
if
(
global
.
redisCache
)
return
global
.
redisCache
;
export
const
getGlobalRedisConnection
=
()
=>
{
if
(
global
.
redisClient
)
return
global
.
redisClient
;
global
.
redisC
ache
=
new
Redis
(
REDIS_URL
,
{
keyPrefix
:
'fastgpt:cache:'
});
global
.
redisC
lient
=
new
Redis
(
REDIS_URL
,
{
keyPrefix
:
FASTGPT_REDIS_PREFIX
});
global
.
redisC
ache
.
on
(
'connect'
,
()
=>
{
global
.
redisC
lient
.
on
(
'connect'
,
()
=>
{
addLog
.
info
(
'Redis connected'
);
addLog
.
info
(
'Redis connected'
);
});
});
global
.
redisC
ache
.
on
(
'error'
,
(
error
)
=>
{
global
.
redisC
lient
.
on
(
'error'
,
(
error
)
=>
{
addLog
.
error
(
'Redis connection error'
,
error
);
addLog
.
error
(
'Redis connection error'
,
error
);
});
});
return
global
.
redisCache
;
return
global
.
redisClient
;
};
export
const
getAllKeysByPrefix
=
async
(
key
:
string
)
=>
{
const
redis
=
getGlobalRedisConnection
();
const
keys
=
(
await
redis
.
keys
(
`
${
FASTGPT_REDIS_PREFIX
}${
key
}
:*`
)).
map
((
key
)
=>
key
.
replace
(
FASTGPT_REDIS_PREFIX
,
''
)
);
return
keys
;
};
};
packages/service/common/redis/type.d.ts
View file @
a4db03a3
import
type
Redis
from
'ioredis'
;
import
type
Redis
from
'ioredis'
;
declare
global
{
declare
global
{
var
redisC
ache
:
Redis
|
null
;
var
redisC
lient
:
Redis
|
null
;
}
}
packages/service/core/ai/utils.ts
View file @
a4db03a3
...
@@ -135,8 +135,8 @@ export const llmStreamResponseToAnswerText = async (
...
@@ -135,8 +135,8 @@ export const llmStreamResponseToAnswerText = async (
// Tool calls
// Tool calls
if
(
responseChoice
?.
tool_calls
?.
length
)
{
if
(
responseChoice
?.
tool_calls
?.
length
)
{
responseChoice
.
tool_calls
.
forEach
((
toolCall
)
=>
{
responseChoice
.
tool_calls
.
forEach
((
toolCall
,
i
)
=>
{
const
index
=
toolCall
.
index
;
const
index
=
toolCall
.
index
??
i
;
if
(
toolCall
.
id
||
callingTool
)
{
if
(
toolCall
.
id
||
callingTool
)
{
// 有 id,代表新 call 工具
// 有 id,代表新 call 工具
...
...
packages/service/support/permission/controller.ts
View file @
a4db03a3
...
@@ -20,6 +20,7 @@ import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/m
...
@@ -20,6 +20,7 @@ import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/m
import
{
type
TeamMemberSchema
}
from
'@fastgpt/global/support/user/team/type'
;
import
{
type
TeamMemberSchema
}
from
'@fastgpt/global/support/user/team/type'
;
import
{
type
OrgSchemaType
}
from
'@fastgpt/global/support/user/team/org/type'
;
import
{
type
OrgSchemaType
}
from
'@fastgpt/global/support/user/team/org/type'
;
import
{
getOrgIdSetWithParentByTmbId
}
from
'./org/controllers'
;
import
{
getOrgIdSetWithParentByTmbId
}
from
'./org/controllers'
;
import
{
authUserSession
}
from
'../user/session'
;
/** get resource permission for a team member
/** get resource permission for a team member
* If there is no permission for the team member, it will return undefined
* If there is no permission for the team member, it will return undefined
...
@@ -213,51 +214,6 @@ export const delResourcePermission = ({
...
@@ -213,51 +214,6 @@ export const delResourcePermission = ({
};
};
/* 下面代码等迁移 */
/* 下面代码等迁移 */
/* create token */
export
function
createJWT
(
user
:
{
_id
?:
string
;
team
?:
{
teamId
?:
string
;
tmbId
:
string
};
isRoot
?:
boolean
;
})
{
const
key
=
process
.
env
.
TOKEN_KEY
as
string
;
const
token
=
jwt
.
sign
(
{
userId
:
String
(
user
.
_id
),
teamId
:
String
(
user
.
team
?.
teamId
),
tmbId
:
String
(
user
.
team
?.
tmbId
),
isRoot
:
user
.
isRoot
,
exp
:
Math
.
floor
(
Date
.
now
()
/
1000
)
+
60
*
60
*
24
*
7
},
key
);
return
token
;
}
// auth token
export
function
authJWT
(
token
:
string
)
{
return
new
Promise
<
{
userId
:
string
;
teamId
:
string
;
tmbId
:
string
;
isRoot
:
boolean
;
}
>
((
resolve
,
reject
)
=>
{
const
key
=
process
.
env
.
TOKEN_KEY
as
string
;
jwt
.
verify
(
token
,
key
,
(
err
,
decoded
:
any
)
=>
{
if
(
err
||
!
decoded
?.
userId
)
{
reject
(
ERROR_ENUM
.
unAuthorization
);
return
;
}
resolve
({
userId
:
decoded
.
userId
,
teamId
:
decoded
.
teamId
||
''
,
tmbId
:
decoded
.
tmbId
,
isRoot
:
decoded
.
isRoot
});
});
});
}
export
async
function
parseHeaderCert
({
export
async
function
parseHeaderCert
({
req
,
req
,
...
@@ -275,7 +231,7 @@ export async function parseHeaderCert({
...
@@ -275,7 +231,7 @@ export async function parseHeaderCert({
return
Promise
.
reject
(
ERROR_ENUM
.
unAuthorization
);
return
Promise
.
reject
(
ERROR_ENUM
.
unAuthorization
);
}
}
return
a
wait
authJWT
(
cookieToken
);
return
a
uthUserSession
(
cookieToken
);
}
}
// from authorization get apikey
// from authorization get apikey
async
function
parseAuthorization
(
authorization
?:
string
)
{
async
function
parseAuthorization
(
authorization
?:
string
)
{
...
@@ -345,6 +301,7 @@ export async function parseHeaderCert({
...
@@ -345,6 +301,7 @@ export async function parseHeaderCert({
if
(
authToken
&&
(
token
||
cookie
))
{
if
(
authToken
&&
(
token
||
cookie
))
{
// user token(from fastgpt web)
// user token(from fastgpt web)
const
res
=
await
authCookieToken
(
cookie
,
token
);
const
res
=
await
authCookieToken
(
cookie
,
token
);
return
{
return
{
uid
:
res
.
userId
,
uid
:
res
.
userId
,
teamId
:
res
.
teamId
,
teamId
:
res
.
teamId
,
...
...
packages/service/support/user/session.ts
0 → 100644
View file @
a4db03a3
import
{
retryFn
}
from
'@fastgpt/global/common/system/utils'
;
import
{
getAllKeysByPrefix
,
getGlobalRedisConnection
}
from
'../../common/redis'
;
import
{
addLog
}
from
'../../common/system/log'
;
import
{
ERROR_ENUM
}
from
'@fastgpt/global/common/error/errorCode'
;
import
{
getNanoid
}
from
'@fastgpt/global/common/string/tools'
;
const
redisPrefix
=
'session:'
;
const
getSessionKey
=
(
key
:
string
)
=>
`
${
redisPrefix
}${
key
}
`
;
type
SessionType
=
{
userId
:
string
;
teamId
:
string
;
tmbId
:
string
;
isRoot
?:
boolean
;
createdAt
:
number
;
ip
?:
string
|
null
;
};
/* Session manager */
const
setSession
=
async
({
key
,
data
,
expireSeconds
}:
{
key
:
string
;
data
:
SessionType
;
expireSeconds
:
number
;
})
=>
{
return
await
retryFn
(
async
()
=>
{
try
{
const
redis
=
getGlobalRedisConnection
();
const
formatKey
=
getSessionKey
(
key
);
// 使用 hmset 存储对象字段
await
redis
.
hmset
(
formatKey
,
{
userId
:
data
.
userId
,
teamId
:
data
.
teamId
,
tmbId
:
data
.
tmbId
,
isRoot
:
data
.
isRoot
?
'1'
:
'0'
,
createdAt
:
data
.
createdAt
.
toString
(),
ip
:
data
.
ip
});
// 设置过期时间
if
(
expireSeconds
)
{
await
redis
.
expire
(
formatKey
,
expireSeconds
);
}
}
catch
(
error
)
{
addLog
.
error
(
'Set session error:'
,
error
);
return
Promise
.
reject
(
error
);
}
});
};
const
delSession
=
(
key
:
string
)
=>
{
const
redis
=
getGlobalRedisConnection
();
retryFn
(()
=>
redis
.
del
(
getSessionKey
(
key
)));
};
const
getSession
=
async
(
key
:
string
):
Promise
<
SessionType
>
=>
{
const
formatKey
=
getSessionKey
(
key
);
const
redis
=
getGlobalRedisConnection
();
// 使用 hgetall 获取所有字段
const
data
=
await
retryFn
(()
=>
redis
.
hgetall
(
formatKey
));
if
(
!
data
||
Object
.
keys
(
data
).
length
===
0
)
{
return
Promise
.
reject
(
ERROR_ENUM
.
unAuthorization
);
}
try
{
return
{
userId
:
data
.
userId
,
teamId
:
data
.
teamId
,
tmbId
:
data
.
tmbId
,
isRoot
:
data
.
isRoot
===
'1'
,
createdAt
:
parseInt
(
data
.
createdAt
),
ip
:
data
.
ip
};
}
catch
(
error
)
{
addLog
.
error
(
'Parse session error:'
,
error
);
delSession
(
formatKey
);
return
Promise
.
reject
(
ERROR_ENUM
.
unAuthorization
);
}
};
export
const
delUserAllSession
=
async
(
userId
:
string
,
whileList
?:
string
[])
=>
{
const
formatWhileList
=
whileList
?.
map
((
item
)
=>
getSessionKey
(
item
));
const
redis
=
getGlobalRedisConnection
();
const
keys
=
(
await
getAllKeysByPrefix
(
`
${
redisPrefix
}${
userId
}
`
)).
filter
(
(
item
)
=>
!
formatWhileList
?.
includes
(
item
)
);
if
(
keys
.
length
>
0
)
{
await
redis
.
del
(
keys
);
}
};
// 会根据创建时间,删除超出客户端登录限制的 session
const
delRedundantSession
=
async
(
userId
:
string
)
=>
{
// 至少为 1,默认为 10
let
maxSession
=
process
.
env
.
MAX_LOGIN_SESSION
?
Number
(
process
.
env
.
MAX_LOGIN_SESSION
)
:
10
;
if
(
maxSession
<
1
)
{
maxSession
=
1
;
}
const
redis
=
getGlobalRedisConnection
();
const
keys
=
await
getAllKeysByPrefix
(
`
${
redisPrefix
}${
userId
}
`
);
if
(
keys
.
length
<=
maxSession
)
{
return
;
}
// 获取所有会话的创建时间
const
sessionList
=
await
Promise
.
all
(
keys
.
map
(
async
(
key
)
=>
{
try
{
const
data
=
await
redis
.
hgetall
(
key
);
if
(
!
data
||
Object
.
keys
(
data
).
length
===
0
)
return
null
;
return
{
key
,
createdAt
:
parseInt
(
data
.
createdAt
)
};
}
catch
(
error
)
{
return
null
;
}
})
);
// 过滤掉无效数据并按创建时间排序
const
validSessions
=
sessionList
.
filter
(
Boolean
)
as
{
key
:
string
;
createdAt
:
number
}[];
validSessions
.
sort
((
a
,
b
)
=>
a
.
createdAt
-
b
.
createdAt
);
// 删除最早创建的会话
const
delKeys
=
validSessions
.
slice
(
0
,
validSessions
.
length
-
maxSession
).
map
((
item
)
=>
item
.
key
);
if
(
delKeys
.
length
>
0
)
{
await
redis
.
del
(
delKeys
);
}
};
export
const
createUserSession
=
async
({
userId
,
teamId
,
tmbId
,
isRoot
,
ip
}:
{
userId
:
string
;
teamId
:
string
;
tmbId
:
string
;
isRoot
?:
boolean
;
ip
?:
string
|
null
;
})
=>
{
const
key
=
`
${
String
(
userId
)}
:
${
getNanoid
(
32
)}
`
;
await
setSession
({
key
,
data
:
{
userId
:
String
(
userId
),
teamId
:
String
(
teamId
),
tmbId
:
String
(
tmbId
),
isRoot
,
createdAt
:
new
Date
().
getTime
(),
ip
},
expireSeconds
:
7
*
24
*
60
*
60
});
delRedundantSession
(
userId
);
return
key
;
};
export
const
authUserSession
=
async
(
key
:
string
):
Promise
<
SessionType
>
=>
{
const
data
=
await
getSession
(
key
);
return
data
;
};
projects/app/.env.template
View file @
a4db03a3
...
@@ -3,8 +3,6 @@ LOG_DEPTH=3
...
@@ -3,8 +3,6 @@ LOG_DEPTH=3
DEFAULT_ROOT_PSW=123456
DEFAULT_ROOT_PSW=123456
# 数据库最大连接数
# 数据库最大连接数
DB_MAX_LINK=5
DB_MAX_LINK=5
# token
TOKEN_KEY=dfdasfdas
# 文件阅读时的密钥
# 文件阅读时的密钥
FILE_TOKEN_KEY=filetokenkey
FILE_TOKEN_KEY=filetokenkey
# root key, 最高权限
# root key, 最高权限
...
@@ -65,6 +63,8 @@ CHECK_INTERNAL_IP=false
...
@@ -65,6 +63,8 @@ CHECK_INTERNAL_IP=false
PASSWORD_LOGIN_LOCK_SECONDS=
PASSWORD_LOGIN_LOCK_SECONDS=
# 密码过期月份,不设置则不会过期
# 密码过期月份,不设置则不会过期
PASSWORD_EXPIRED_MONTH=
PASSWORD_EXPIRED_MONTH=
# 最大登录客户端数量,默认为 10
MAX_LOGIN_SESSION=
# 特殊配置
# 特殊配置
# 自定义跨域,不配置时,默认都允许跨域(逗号分割)
# 自定义跨域,不配置时,默认都允许跨域(逗号分割)
...
...
projects/app/src/pages/api/support/user/account/loginByPassword.ts
View file @
a4db03a3
import
type
{
NextApiRequest
,
NextApiResponse
}
from
'next'
;
import
type
{
NextApiRequest
,
NextApiResponse
}
from
'next'
;
import
{
MongoUser
}
from
'@fastgpt/service/support/user/schema'
;
import
{
MongoUser
}
from
'@fastgpt/service/support/user/schema'
;
import
{
createJWT
,
setCookie
}
from
'@fastgpt/service/support/permission/controller'
;
import
{
setCookie
}
from
'@fastgpt/service/support/permission/controller'
;
import
{
getUserDetail
}
from
'@fastgpt/service/support/user/controller'
;
import
{
getUserDetail
}
from
'@fastgpt/service/support/user/controller'
;
import
type
{
PostLoginProps
}
from
'@fastgpt/global/support/user/api.d'
;
import
type
{
PostLoginProps
}
from
'@fastgpt/global/support/user/api.d'
;
import
{
UserStatusEnum
}
from
'@fastgpt/global/support/user/constant'
;
import
{
UserStatusEnum
}
from
'@fastgpt/global/support/user/constant'
;
...
@@ -13,6 +13,8 @@ import { addOperationLog } from '@fastgpt/service/support/operationLog/addOperat
...
@@ -13,6 +13,8 @@ import { addOperationLog } from '@fastgpt/service/support/operationLog/addOperat
import
{
OperationLogEventEnum
}
from
'@fastgpt/global/support/operationLog/constants'
;
import
{
OperationLogEventEnum
}
from
'@fastgpt/global/support/operationLog/constants'
;
import
{
UserAuthTypeEnum
}
from
'@fastgpt/global/support/user/auth/constants'
;
import
{
UserAuthTypeEnum
}
from
'@fastgpt/global/support/user/auth/constants'
;
import
{
authCode
}
from
'@fastgpt/service/support/user/auth/controller'
;
import
{
authCode
}
from
'@fastgpt/service/support/user/auth/controller'
;
import
{
createUserSession
}
from
'@fastgpt/service/support/user/session'
;
import
requestIp
from
'request-ip'
;
async
function
handler
(
req
:
NextApiRequest
,
res
:
NextApiResponse
)
{
async
function
handler
(
req
:
NextApiRequest
,
res
:
NextApiResponse
)
{
const
{
username
,
password
,
code
}
=
req
.
body
as
PostLoginProps
;
const
{
username
,
password
,
code
}
=
req
.
body
as
PostLoginProps
;
...
@@ -61,20 +63,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
...
@@ -61,20 +63,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
lastLoginTmbId
:
userDetail
.
team
.
tmbId
lastLoginTmbId
:
userDetail
.
team
.
tmbId
});
});
const
token
=
await
createUserSession
({
userId
:
user
.
_id
,
teamId
:
userDetail
.
team
.
teamId
,
tmbId
:
userDetail
.
team
.
tmbId
,
isRoot
:
username
===
'root'
,
ip
:
requestIp
.
getClientIp
(
req
)
});
setCookie
(
res
,
token
);
pushTrack
.
login
({
pushTrack
.
login
({
type
:
'password'
,
type
:
'password'
,
uid
:
user
.
_id
,
uid
:
user
.
_id
,
teamId
:
userDetail
.
team
.
teamId
,
teamId
:
userDetail
.
team
.
teamId
,
tmbId
:
userDetail
.
team
.
tmbId
tmbId
:
userDetail
.
team
.
tmbId
});
});
const
token
=
createJWT
({
...
userDetail
,
isRoot
:
username
===
'root'
});
setCookie
(
res
,
token
);
addOperationLog
({
addOperationLog
({
tmbId
:
userDetail
.
team
.
tmbId
,
tmbId
:
userDetail
.
team
.
tmbId
,
teamId
:
userDetail
.
team
.
teamId
,
teamId
:
userDetail
.
team
.
teamId
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment