Update asset metadata and enhance collection features in the API
- Updated the modification times for various assets in `bindata.go` to reflect recent changes. - Enhanced the collection management in the API by adding new fields for `preset`, `public`, and `share` in the collection and document models, allowing for better control over collection visibility and sharing options. - Updated the `CreateCollection` function to incorporate the new `share` field, improving the handling of collection data based on user permissions. - Refactored utility functions to streamline type conversions, ensuring consistent data handling across the user and team management functionalities.
This commit is contained in:
parent
0ee38a1cb3
commit
d17832c016
12 changed files with 622 additions and 577 deletions
286
data/bindata.go
286
data/bindata.go
File diff suppressed because one or more lines are too long
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/yaoapp/kun/log"
|
"github.com/yaoapp/kun/log"
|
||||||
"github.com/yaoapp/kun/maps"
|
"github.com/yaoapp/kun/maps"
|
||||||
"github.com/yaoapp/yao/kb"
|
"github.com/yaoapp/yao/kb"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||||
"github.com/yaoapp/yao/openapi/response"
|
"github.com/yaoapp/yao/openapi/response"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -22,7 +23,7 @@ var (
|
||||||
// availableCollectionFields defines all available fields for security filtering
|
// availableCollectionFields defines all available fields for security filtering
|
||||||
availableCollectionFields = map[string]bool{
|
availableCollectionFields = map[string]bool{
|
||||||
"id": true, "collection_id": true, "name": true, "description": true,
|
"id": true, "collection_id": true, "name": true, "description": true,
|
||||||
"status": true, "system": true, "readonly": true, "sort": true, "cover": true,
|
"status": true, "preset": true, "public": true, "share": true, "sort": true, "cover": true,
|
||||||
"document_count": true, "embedding_provider_id": true, "embedding_option_id": true,
|
"document_count": true, "embedding_provider_id": true, "embedding_option_id": true,
|
||||||
"embedding_properties": true, "locale": true, "dimension": true,
|
"embedding_properties": true, "locale": true, "dimension": true,
|
||||||
"distance_metric": true, "hnsw_m": true, "ef_construction": true,
|
"distance_metric": true, "hnsw_m": true, "ef_construction": true,
|
||||||
|
|
@ -32,7 +33,7 @@ var (
|
||||||
|
|
||||||
// defaultCollectionFields defines the default compact field list
|
// defaultCollectionFields defines the default compact field list
|
||||||
defaultCollectionFields = []interface{}{
|
defaultCollectionFields = []interface{}{
|
||||||
"id", "collection_id", "name", "description", "status", "system", "readonly",
|
"id", "collection_id", "name", "description", "status", "preset", "public", "share",
|
||||||
"sort", "cover", "document_count", "embedding_provider_id", "embedding_option_id",
|
"sort", "cover", "document_count", "embedding_provider_id", "embedding_option_id",
|
||||||
"locale", "dimension", "distance_metric", "created_at", "updated_at",
|
"locale", "dimension", "distance_metric", "created_at", "updated_at",
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +58,7 @@ type ProviderSettings struct {
|
||||||
|
|
||||||
// CreateCollection creates a new collection
|
// CreateCollection creates a new collection
|
||||||
func CreateCollection(c *gin.Context) {
|
func CreateCollection(c *gin.Context) {
|
||||||
|
|
||||||
// Prepare request and database data
|
// Prepare request and database data
|
||||||
req, collectionData, err := PrepareCreateCollection(c)
|
req, collectionData, err := PrepareCreateCollection(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -68,6 +70,12 @@ func CreateCollection(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attach create scope to the collection data
|
||||||
|
authInfo := authorized.GetInfo(c)
|
||||||
|
if authInfo != nil {
|
||||||
|
collectionData = authInfo.WithCreateScope(collectionData)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if kb.Instance is available
|
// Check if kb.Instance is available
|
||||||
if kb.Instance == nil {
|
if kb.Instance == nil {
|
||||||
errorResp := &response.ErrorResponse{
|
errorResp := &response.ErrorResponse{
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/yaoapp/yao/attachment"
|
"github.com/yaoapp/yao/attachment"
|
||||||
"github.com/yaoapp/yao/kb"
|
"github.com/yaoapp/yao/kb"
|
||||||
kbtypes "github.com/yaoapp/yao/kb/types"
|
kbtypes "github.com/yaoapp/yao/kb/types"
|
||||||
|
apiutils "github.com/yaoapp/yao/openapi/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrepareCreateCollection prepares CreateCollection request and database data
|
// PrepareCreateCollection prepares CreateCollection request and database data
|
||||||
|
|
@ -70,6 +71,12 @@ func PrepareCreateCollection(c *gin.Context) (*CreateCollectionRequest, map[stri
|
||||||
"index_type": req.Config.IndexType,
|
"index_type": req.Config.IndexType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add share field from metadata if provided
|
||||||
|
share := apiutils.ToString(req.Metadata["share"])
|
||||||
|
if share == "private" || share == "team" {
|
||||||
|
data["share"] = share
|
||||||
|
}
|
||||||
|
|
||||||
// Add optional HNSW parameters
|
// Add optional HNSW parameters
|
||||||
if req.Config.M > 0 {
|
if req.Config.M > 0 {
|
||||||
data["m"] = req.Config.M
|
data["m"] = req.Config.M
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
||||||
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
"github.com/yaoapp/yao/openapi/response"
|
"github.com/yaoapp/yao/openapi/response"
|
||||||
|
"github.com/yaoapp/yao/openapi/utils"
|
||||||
"github.com/yaoapp/yao/utils/captcha"
|
"github.com/yaoapp/yao/utils/captcha"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -195,7 +196,7 @@ func LoginByUserID(userid string, loginCtx *LoginContext) (*LoginResponse, error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get MFA enabled status from user data
|
// Get MFA enabled status from user data
|
||||||
mfaEnabled := toBool(user["mfa_enabled"])
|
mfaEnabled := utils.ToBool(user["mfa_enabled"])
|
||||||
|
|
||||||
// If MFA enabled, generate MFA token
|
// If MFA enabled, generate MFA token
|
||||||
if mfaEnabled {
|
if mfaEnabled {
|
||||||
|
|
@ -459,7 +460,7 @@ func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse
|
||||||
extraClaims["team_id"] = params.TeamID
|
extraClaims["team_id"] = params.TeamID
|
||||||
|
|
||||||
// Add tenant_id if available from the team
|
// Add tenant_id if available from the team
|
||||||
if tenantID := toString(params.Team["tenant_id"]); tenantID != "" {
|
if tenantID := utils.ToString(params.Team["tenant_id"]); tenantID != "" {
|
||||||
extraClaims["tenant_id"] = tenantID
|
extraClaims["tenant_id"] = tenantID
|
||||||
oidcUserInfo.YaoTenantID = tenantID
|
oidcUserInfo.YaoTenantID = tenantID
|
||||||
}
|
}
|
||||||
|
|
@ -467,21 +468,21 @@ func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse
|
||||||
// Add team info to OIDC user info
|
// Add team info to OIDC user info
|
||||||
oidcUserInfo.YaoTeamID = params.TeamID
|
oidcUserInfo.YaoTeamID = params.TeamID
|
||||||
teamInfo := &oauthtypes.OIDCTeamInfo{}
|
teamInfo := &oauthtypes.OIDCTeamInfo{}
|
||||||
if teamIDVal := toString(params.Team["team_id"]); teamIDVal != "" {
|
if teamIDVal := utils.ToString(params.Team["team_id"]); teamIDVal != "" {
|
||||||
teamInfo.TeamID = teamIDVal
|
teamInfo.TeamID = teamIDVal
|
||||||
}
|
}
|
||||||
if logo := toString(params.Team["logo"]); logo != "" {
|
if logo := utils.ToString(params.Team["logo"]); logo != "" {
|
||||||
teamInfo.Logo = logo
|
teamInfo.Logo = logo
|
||||||
}
|
}
|
||||||
if name := toString(params.Team["name"]); name != "" {
|
if name := utils.ToString(params.Team["name"]); name != "" {
|
||||||
teamInfo.Name = name
|
teamInfo.Name = name
|
||||||
}
|
}
|
||||||
if description := toString(params.Team["description"]); description != "" {
|
if description := utils.ToString(params.Team["description"]); description != "" {
|
||||||
teamInfo.Description = description
|
teamInfo.Description = description
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add owner_id if available from the team (only check once)
|
// Add owner_id if available from the team (only check once)
|
||||||
if ownerID := toString(params.Team["owner_id"]); ownerID != "" {
|
if ownerID := utils.ToString(params.Team["owner_id"]); ownerID != "" {
|
||||||
extraClaims["owner_id"] = ownerID
|
extraClaims["owner_id"] = ownerID
|
||||||
teamInfo.OwnerID = ownerID
|
teamInfo.OwnerID = ownerID
|
||||||
|
|
||||||
|
|
@ -497,19 +498,19 @@ func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse
|
||||||
// Add member profile information if available
|
// Add member profile information if available
|
||||||
if params.Member != nil {
|
if params.Member != nil {
|
||||||
memberInfo := &oauthtypes.OIDCMemberInfo{}
|
memberInfo := &oauthtypes.OIDCMemberInfo{}
|
||||||
if memberID := toString(params.Member["member_id"]); memberID != "" {
|
if memberID := utils.ToString(params.Member["member_id"]); memberID != "" {
|
||||||
memberInfo.MemberID = memberID
|
memberInfo.MemberID = memberID
|
||||||
}
|
}
|
||||||
if displayName := toString(params.Member["display_name"]); displayName != "" {
|
if displayName := utils.ToString(params.Member["display_name"]); displayName != "" {
|
||||||
memberInfo.DisplayName = displayName
|
memberInfo.DisplayName = displayName
|
||||||
}
|
}
|
||||||
if bio := toString(params.Member["bio"]); bio != "" {
|
if bio := utils.ToString(params.Member["bio"]); bio != "" {
|
||||||
memberInfo.Bio = bio
|
memberInfo.Bio = bio
|
||||||
}
|
}
|
||||||
if avatar := toString(params.Member["avatar"]); avatar != "" {
|
if avatar := utils.ToString(params.Member["avatar"]); avatar != "" {
|
||||||
memberInfo.Avatar = avatar
|
memberInfo.Avatar = avatar
|
||||||
}
|
}
|
||||||
if email := toString(params.Member["email"]); email != "" {
|
if email := utils.ToString(params.Member["email"]); email != "" {
|
||||||
memberInfo.Email = email
|
memberInfo.Email = email
|
||||||
}
|
}
|
||||||
oidcUserInfo.YaoMember = memberInfo
|
oidcUserInfo.YaoMember = memberInfo
|
||||||
|
|
@ -520,10 +521,10 @@ func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse
|
||||||
var typeID string
|
var typeID string
|
||||||
if params.TeamID != "" && params.Team != nil {
|
if params.TeamID != "" && params.Team != nil {
|
||||||
// Team context - use team's type
|
// Team context - use team's type
|
||||||
typeID = toString(params.Team["type_id"])
|
typeID = utils.ToString(params.Team["type_id"])
|
||||||
} else {
|
} else {
|
||||||
// Personal context - use user's type
|
// Personal context - use user's type
|
||||||
typeID = toString(params.User["type_id"])
|
typeID = utils.ToString(params.User["type_id"])
|
||||||
}
|
}
|
||||||
|
|
||||||
if typeID != "" {
|
if typeID != "" {
|
||||||
|
|
@ -538,13 +539,13 @@ func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse
|
||||||
if err == nil && typeInfo != nil {
|
if err == nil && typeInfo != nil {
|
||||||
// Add type info to OIDC user info
|
// Add type info to OIDC user info
|
||||||
typeDetails := &oauthtypes.OIDCTypeInfo{}
|
typeDetails := &oauthtypes.OIDCTypeInfo{}
|
||||||
if typeIDVal := toString(typeInfo["type_id"]); typeIDVal != "" {
|
if typeIDVal := utils.ToString(typeInfo["type_id"]); typeIDVal != "" {
|
||||||
typeDetails.TypeID = typeIDVal
|
typeDetails.TypeID = typeIDVal
|
||||||
}
|
}
|
||||||
if name := toString(typeInfo["name"]); name != "" {
|
if name := utils.ToString(typeInfo["name"]); name != "" {
|
||||||
typeDetails.Name = name
|
typeDetails.Name = name
|
||||||
}
|
}
|
||||||
if locale := toString(typeInfo["locale"]); locale != "" {
|
if locale := utils.ToString(typeInfo["locale"]); locale != "" {
|
||||||
typeDetails.Locale = locale
|
typeDetails.Locale = locale
|
||||||
}
|
}
|
||||||
oidcUserInfo.YaoType = typeDetails
|
oidcUserInfo.YaoType = typeDetails
|
||||||
|
|
@ -595,7 +596,7 @@ func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse
|
||||||
ExpiresIn: expiresIn,
|
ExpiresIn: expiresIn,
|
||||||
RefreshTokenExpiresIn: refreshTokenExpiresIn,
|
RefreshTokenExpiresIn: refreshTokenExpiresIn,
|
||||||
TokenType: "Bearer",
|
TokenType: "Bearer",
|
||||||
MFAEnabled: toBool(params.User["mfa_enabled"]),
|
MFAEnabled: utils.ToBool(params.User["mfa_enabled"]),
|
||||||
Scope: strings.Join(params.Scopes, " "),
|
Scope: strings.Join(params.Scopes, " "),
|
||||||
Status: LoginStatusSuccess,
|
Status: LoginStatusSuccess,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/yaoapp/yao/openapi/oauth"
|
"github.com/yaoapp/yao/openapi/oauth"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||||
"github.com/yaoapp/yao/openapi/response"
|
"github.com/yaoapp/yao/openapi/response"
|
||||||
|
"github.com/yaoapp/yao/openapi/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Member Management Handlers
|
// Member Management Handlers
|
||||||
|
|
@ -292,7 +293,7 @@ func GinMemberCreateRobot(c *gin.Context) {
|
||||||
"bio": req.Bio,
|
"bio": req.Bio,
|
||||||
"role_id": req.RoleID,
|
"role_id": req.RoleID,
|
||||||
"system_prompt": req.SystemPrompt,
|
"system_prompt": req.SystemPrompt,
|
||||||
"autonomous_mode": toBool(req.AutonomousMode),
|
"autonomous_mode": utils.ToBool(req.AutonomousMode),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add optional fields
|
// Add optional fields
|
||||||
|
|
@ -437,7 +438,7 @@ func GinMemberUpdateRobot(c *gin.Context) {
|
||||||
updateData["robot_status"] = req.RobotStatus
|
updateData["robot_status"] = req.RobotStatus
|
||||||
}
|
}
|
||||||
if req.AutonomousMode != "" {
|
if req.AutonomousMode != "" {
|
||||||
updateData["autonomous_mode"] = toBool(req.AutonomousMode)
|
updateData["autonomous_mode"] = utils.ToBool(req.AutonomousMode)
|
||||||
}
|
}
|
||||||
if req.CostLimit > 0 {
|
if req.CostLimit > 0 {
|
||||||
updateData["cost_limit"] = req.CostLimit
|
updateData["cost_limit"] = req.CostLimit
|
||||||
|
|
@ -1527,29 +1528,29 @@ func checkTeamAccess(ctx context.Context, teamID, userID string) (bool, bool, er
|
||||||
// mapToMemberResponse converts a map to MemberResponse
|
// mapToMemberResponse converts a map to MemberResponse
|
||||||
func mapToMemberResponse(data maps.MapStr) MemberResponse {
|
func mapToMemberResponse(data maps.MapStr) MemberResponse {
|
||||||
member := MemberResponse{
|
member := MemberResponse{
|
||||||
ID: toInt64(data["id"]),
|
ID: utils.ToInt64(data["id"]),
|
||||||
MemberID: toString(data["member_id"]),
|
MemberID: utils.ToString(data["member_id"]),
|
||||||
TeamID: toString(data["team_id"]),
|
TeamID: utils.ToString(data["team_id"]),
|
||||||
UserID: toString(data["user_id"]),
|
UserID: utils.ToString(data["user_id"]),
|
||||||
MemberType: toString(data["member_type"]),
|
MemberType: utils.ToString(data["member_type"]),
|
||||||
DisplayName: toString(data["display_name"]),
|
DisplayName: utils.ToString(data["display_name"]),
|
||||||
Bio: toString(data["bio"]),
|
Bio: utils.ToString(data["bio"]),
|
||||||
Avatar: toString(data["avatar"]),
|
Avatar: utils.ToString(data["avatar"]),
|
||||||
Email: toString(data["email"]),
|
Email: utils.ToString(data["email"]),
|
||||||
RobotEmail: toString(data["robot_email"]), // Globally unique email for robot members
|
RobotEmail: utils.ToString(data["robot_email"]), // Globally unique email for robot members
|
||||||
RoleID: toString(data["role_id"]),
|
RoleID: utils.ToString(data["role_id"]),
|
||||||
IsOwner: data["is_owner"], // Keep original type (int or bool)
|
IsOwner: data["is_owner"], // Keep original type (int or bool)
|
||||||
Status: toString(data["status"]),
|
Status: utils.ToString(data["status"]),
|
||||||
InvitationID: toString(data["invitation_id"]),
|
InvitationID: utils.ToString(data["invitation_id"]),
|
||||||
InvitedBy: toString(data["invited_by"]),
|
InvitedBy: utils.ToString(data["invited_by"]),
|
||||||
InvitedAt: toTimeString(data["invited_at"]),
|
InvitedAt: utils.ToTimeString(data["invited_at"]),
|
||||||
InvitationToken: toString(data["invitation_token"]),
|
InvitationToken: utils.ToString(data["invitation_token"]),
|
||||||
InvitationExpiresAt: toTimeString(data["invitation_expires_at"]),
|
InvitationExpiresAt: utils.ToTimeString(data["invitation_expires_at"]),
|
||||||
JoinedAt: toTimeString(data["joined_at"]),
|
JoinedAt: utils.ToTimeString(data["joined_at"]),
|
||||||
LastActiveAt: toTimeString(data["last_active_at"]),
|
LastActiveAt: utils.ToTimeString(data["last_active_at"]),
|
||||||
LoginCount: toInt(data["login_count"]),
|
LoginCount: utils.ToInt(data["login_count"]),
|
||||||
CreatedAt: toTimeString(data["created_at"]),
|
CreatedAt: utils.ToTimeString(data["created_at"]),
|
||||||
UpdatedAt: toTimeString(data["updated_at"]),
|
UpdatedAt: utils.ToTimeString(data["updated_at"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add settings if available
|
// Add settings if available
|
||||||
|
|
@ -1559,7 +1560,7 @@ func mapToMemberResponse(data maps.MapStr) MemberResponse {
|
||||||
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
||||||
// Convert map to MemberSettings (for backward compatibility)
|
// Convert map to MemberSettings (for backward compatibility)
|
||||||
memSettings := &MemberSettings{
|
memSettings := &MemberSettings{
|
||||||
Notifications: toBool(settingsMap["notifications"]),
|
Notifications: utils.ToBool(settingsMap["notifications"]),
|
||||||
}
|
}
|
||||||
// Handle permissions array
|
// Handle permissions array
|
||||||
if perms, ok := settingsMap["permissions"]; ok {
|
if perms, ok := settingsMap["permissions"]; ok {
|
||||||
|
|
@ -1587,14 +1588,14 @@ func mapToMemberDetailResponse(data maps.MapStr) MemberDetailResponse {
|
||||||
member := MemberDetailResponse{
|
member := MemberDetailResponse{
|
||||||
MemberResponse: mapToMemberResponse(data),
|
MemberResponse: mapToMemberResponse(data),
|
||||||
// Robot-specific fields
|
// Robot-specific fields
|
||||||
SystemPrompt: toString(data["system_prompt"]),
|
SystemPrompt: utils.ToString(data["system_prompt"]),
|
||||||
ManagerID: toString(data["manager_id"]),
|
ManagerID: utils.ToString(data["manager_id"]),
|
||||||
LanguageModel: toString(data["language_model"]),
|
LanguageModel: utils.ToString(data["language_model"]),
|
||||||
CostLimit: toFloat64(data["cost_limit"]),
|
CostLimit: utils.ToFloat64(data["cost_limit"]),
|
||||||
AutonomousMode: data["autonomous_mode"], // Keep original type (bool or string)
|
AutonomousMode: data["autonomous_mode"], // Keep original type (bool or string)
|
||||||
LastRobotActivity: toTimeString(data["last_robot_activity"]),
|
LastRobotActivity: utils.ToTimeString(data["last_robot_activity"]),
|
||||||
RobotStatus: toString(data["robot_status"]),
|
RobotStatus: utils.ToString(data["robot_status"]),
|
||||||
Notes: toString(data["notes"]),
|
Notes: utils.ToString(data["notes"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle authorized_senders array
|
// Handle authorized_senders array
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
||||||
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
"github.com/yaoapp/yao/openapi/response"
|
"github.com/yaoapp/yao/openapi/response"
|
||||||
|
"github.com/yaoapp/yao/openapi/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User Profile Management Handlers
|
// User Profile Management Handlers
|
||||||
|
|
@ -246,11 +247,11 @@ func addTeamInfo(ctx context.Context, provider *user.DefaultUser, oidcUserInfo *
|
||||||
if withTeam {
|
if withTeam {
|
||||||
oidcUserInfo.YaoTeamID = teamID
|
oidcUserInfo.YaoTeamID = teamID
|
||||||
oidcUserInfo.YaoTeam = &oauthtypes.OIDCTeamInfo{
|
oidcUserInfo.YaoTeam = &oauthtypes.OIDCTeamInfo{
|
||||||
TeamID: toString(team["team_id"]),
|
TeamID: utils.ToString(team["team_id"]),
|
||||||
Name: toString(team["name"]),
|
Name: utils.ToString(team["name"]),
|
||||||
Description: toString(team["description"]),
|
Description: utils.ToString(team["description"]),
|
||||||
Logo: toString(team["logo"]),
|
Logo: utils.ToString(team["logo"]),
|
||||||
OwnerID: toString(team["owner_id"]),
|
OwnerID: utils.ToString(team["owner_id"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is owner
|
// Check if user is owner
|
||||||
|
|
@ -260,7 +261,7 @@ func addTeamInfo(ctx context.Context, provider *user.DefaultUser, oidcUserInfo *
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add tenant_id if available
|
// Add tenant_id if available
|
||||||
if tenantID := toString(team["tenant_id"]); tenantID != "" {
|
if tenantID := utils.ToString(team["tenant_id"]); tenantID != "" {
|
||||||
oidcUserInfo.YaoTenantID = tenantID
|
oidcUserInfo.YaoTenantID = tenantID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -274,13 +275,13 @@ func addTypeInfo(ctx context.Context, provider *user.DefaultUser, oidcUserInfo *
|
||||||
if teamID != "" {
|
if teamID != "" {
|
||||||
team, err := provider.GetTeamByMember(ctx, teamID, userID)
|
team, err := provider.GetTeamByMember(ctx, teamID, userID)
|
||||||
if err == nil && team != nil {
|
if err == nil && team != nil {
|
||||||
typeID = toString(team["type_id"])
|
typeID = utils.ToString(team["type_id"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to user's type
|
// Fallback to user's type
|
||||||
if typeID == "" {
|
if typeID == "" {
|
||||||
typeID = toString(userData["type_id"])
|
typeID = utils.ToString(userData["type_id"])
|
||||||
}
|
}
|
||||||
|
|
||||||
if typeID == "" {
|
if typeID == "" {
|
||||||
|
|
@ -297,9 +298,9 @@ func addTypeInfo(ctx context.Context, provider *user.DefaultUser, oidcUserInfo *
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcUserInfo.YaoType = &oauthtypes.OIDCTypeInfo{
|
oidcUserInfo.YaoType = &oauthtypes.OIDCTypeInfo{
|
||||||
TypeID: toString(typeInfo["type_id"]),
|
TypeID: utils.ToString(typeInfo["type_id"]),
|
||||||
Name: toString(typeInfo["name"]),
|
Name: utils.ToString(typeInfo["name"]),
|
||||||
Locale: toString(typeInfo["locale"]),
|
Locale: utils.ToString(typeInfo["locale"]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
"github.com/yaoapp/yao/openapi/response"
|
"github.com/yaoapp/yao/openapi/response"
|
||||||
|
"github.com/yaoapp/yao/openapi/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Team Management Handlers
|
// Team Management Handlers
|
||||||
|
|
@ -132,7 +133,7 @@ func GinTeamGet(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Check if user owns this team
|
// // Check if user owns this team
|
||||||
// ownerID := toString(teamData["owner_id"])
|
// ownerID := utils.ToString(teamData["owner_id"])
|
||||||
// if ownerID != authInfo.UserID {
|
// if ownerID != authInfo.UserID {
|
||||||
// errorResp := &response.ErrorResponse{
|
// errorResp := &response.ErrorResponse{
|
||||||
// Code: response.ErrAccessDenied.Code,
|
// Code: response.ErrAccessDenied.Code,
|
||||||
|
|
@ -878,7 +879,7 @@ func teamUpdate(ctx context.Context, userID, teamID string, updateData maps.MapS
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check ownership
|
// Check ownership
|
||||||
ownerID := toString(teamData["owner_id"])
|
ownerID := utils.ToString(teamData["owner_id"])
|
||||||
if ownerID != userID {
|
if ownerID != userID {
|
||||||
return fmt.Errorf("access denied: user does not own this team")
|
return fmt.Errorf("access denied: user does not own this team")
|
||||||
}
|
}
|
||||||
|
|
@ -910,7 +911,7 @@ func teamDelete(ctx context.Context, userID, teamID string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check ownership
|
// Check ownership
|
||||||
ownerID := toString(teamData["owner_id"])
|
ownerID := utils.ToString(teamData["owner_id"])
|
||||||
if ownerID != userID {
|
if ownerID != userID {
|
||||||
return fmt.Errorf("access denied: user does not own this team")
|
return fmt.Errorf("access denied: user does not own this team")
|
||||||
}
|
}
|
||||||
|
|
@ -957,18 +958,18 @@ func getUserProvider() (*user.DefaultUser, error) {
|
||||||
// mapToTeamResponse converts a map to TeamResponse
|
// mapToTeamResponse converts a map to TeamResponse
|
||||||
func mapToTeamResponse(data maps.MapStr) TeamResponse {
|
func mapToTeamResponse(data maps.MapStr) TeamResponse {
|
||||||
team := TeamResponse{
|
team := TeamResponse{
|
||||||
ID: toInt64(data["id"]),
|
ID: utils.ToInt64(data["id"]),
|
||||||
TeamID: toString(data["team_id"]),
|
TeamID: utils.ToString(data["team_id"]),
|
||||||
Name: toString(data["name"]),
|
Name: utils.ToString(data["name"]),
|
||||||
Description: toString(data["description"]),
|
Description: utils.ToString(data["description"]),
|
||||||
Logo: toString(data["logo"]),
|
Logo: utils.ToString(data["logo"]),
|
||||||
OwnerID: toString(data["owner_id"]),
|
OwnerID: utils.ToString(data["owner_id"]),
|
||||||
Status: toString(data["status"]),
|
Status: utils.ToString(data["status"]),
|
||||||
IsVerified: toBool(data["is_verified"]),
|
IsVerified: utils.ToBool(data["is_verified"]),
|
||||||
VerifiedBy: toString(data["verified_by"]),
|
VerifiedBy: utils.ToString(data["verified_by"]),
|
||||||
VerifiedAt: toTimeString(data["verified_at"]),
|
VerifiedAt: utils.ToTimeString(data["verified_at"]),
|
||||||
CreatedAt: toTimeString(data["created_at"]),
|
CreatedAt: utils.ToTimeString(data["created_at"]),
|
||||||
UpdatedAt: toTimeString(data["updated_at"]),
|
UpdatedAt: utils.ToTimeString(data["updated_at"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
return team
|
return team
|
||||||
|
|
@ -987,8 +988,8 @@ func mapToTeamDetailResponse(data maps.MapStr) TeamDetailResponse {
|
||||||
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
||||||
// Convert map to TeamSettings (for backward compatibility)
|
// Convert map to TeamSettings (for backward compatibility)
|
||||||
teamSettings := &TeamSettings{
|
teamSettings := &TeamSettings{
|
||||||
Theme: toString(settingsMap["theme"]),
|
Theme: utils.ToString(settingsMap["theme"]),
|
||||||
Visibility: toString(settingsMap["visibility"]),
|
Visibility: utils.ToString(settingsMap["visibility"]),
|
||||||
}
|
}
|
||||||
team.Settings = teamSettings
|
team.Settings = teamSettings
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/yaoapp/kun/maps"
|
"github.com/yaoapp/kun/maps"
|
||||||
"github.com/yaoapp/yao/attachment"
|
"github.com/yaoapp/yao/attachment"
|
||||||
"github.com/yaoapp/yao/messenger"
|
"github.com/yaoapp/yao/messenger"
|
||||||
|
"github.com/yaoapp/yao/openapi/utils"
|
||||||
messengertypes "github.com/yaoapp/yao/messenger/types"
|
messengertypes "github.com/yaoapp/yao/messenger/types"
|
||||||
"github.com/yaoapp/yao/openapi/oauth"
|
"github.com/yaoapp/yao/openapi/oauth"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||||
|
|
@ -542,7 +543,7 @@ func GinTeamInvitationAccept(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get team_id from invitation
|
// Get team_id from invitation
|
||||||
teamID := toString(invitationData["team_id"])
|
teamID := utils.ToString(invitationData["team_id"])
|
||||||
if teamID == "" {
|
if teamID == "" {
|
||||||
log.Error("Invalid invitation: missing team_id")
|
log.Error("Invalid invitation: missing team_id")
|
||||||
errorResp := &response.ErrorResponse{
|
errorResp := &response.ErrorResponse{
|
||||||
|
|
@ -636,16 +637,16 @@ func ProcessTeamInvitationList(process *process.Process) interface{} {
|
||||||
page := 1
|
page := 1
|
||||||
pagesize := 20
|
pagesize := 20
|
||||||
|
|
||||||
if p := int(toInt64(queryMap["page"])); p > 0 {
|
if p := int(utils.ToInt64(queryMap["page"])); p > 0 {
|
||||||
page = p
|
page = p
|
||||||
}
|
}
|
||||||
|
|
||||||
if ps := int(toInt64(queryMap["pagesize"])); ps > 0 && ps <= 100 {
|
if ps := int(utils.ToInt64(queryMap["pagesize"])); ps > 0 && ps <= 100 {
|
||||||
pagesize = ps
|
pagesize = ps
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get status filter
|
// Get status filter
|
||||||
status := toString(queryMap["status"])
|
status := utils.ToString(queryMap["status"])
|
||||||
|
|
||||||
// Get context
|
// Get context
|
||||||
ctx := process.Context
|
ctx := process.Context
|
||||||
|
|
@ -947,7 +948,7 @@ func teamInvitationGetPublic(ctx context.Context, invitationID, locale string) (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only return if it's a pending invitation
|
// Only return if it's a pending invitation
|
||||||
if toString(invitationData["status"]) != "pending" {
|
if utils.ToString(invitationData["status"]) != "pending" {
|
||||||
return nil, fmt.Errorf("invitation not found or no longer pending")
|
return nil, fmt.Errorf("invitation not found or no longer pending")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -970,7 +971,7 @@ func teamInvitationGetPublic(ctx context.Context, invitationID, locale string) (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get team information
|
// Get team information
|
||||||
teamID := toString(invitationData["team_id"])
|
teamID := utils.ToString(invitationData["team_id"])
|
||||||
team, err := provider.GetTeam(ctx, teamID)
|
team, err := provider.GetTeam(ctx, teamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Failed to get team information: %v", err)
|
log.Warn("Failed to get team information: %v", err)
|
||||||
|
|
@ -978,25 +979,25 @@ func teamInvitationGetPublic(ctx context.Context, invitationID, locale string) (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get inviter information
|
// Get inviter information
|
||||||
inviterID := toString(invitationData["invited_by"])
|
inviterID := utils.ToString(invitationData["invited_by"])
|
||||||
var inviterInfo *InviterInfo
|
var inviterInfo *InviterInfo
|
||||||
if inviterID != "" {
|
if inviterID != "" {
|
||||||
inviter, err := provider.GetUser(ctx, inviterID)
|
inviter, err := provider.GetUser(ctx, inviterID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
inviterInfo = &InviterInfo{
|
inviterInfo = &InviterInfo{
|
||||||
UserID: inviterID,
|
UserID: inviterID,
|
||||||
Name: toString(inviter["name"]),
|
Name: utils.ToString(inviter["name"]),
|
||||||
Picture: toString(inviter["picture"]),
|
Picture: utils.ToString(inviter["picture"]),
|
||||||
}
|
}
|
||||||
// Fallback to masked email if name is empty (for privacy protection)
|
// Fallback to masked email if name is empty (for privacy protection)
|
||||||
if inviterInfo.Name == "" {
|
if inviterInfo.Name == "" {
|
||||||
inviterInfo.Name = maskEmail(toString(inviter["email"]))
|
inviterInfo.Name = maskEmail(utils.ToString(inviter["email"]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get role label from team config using provided locale
|
// Get role label from team config using provided locale
|
||||||
roleID := toString(invitationData["role_id"])
|
roleID := utils.ToString(invitationData["role_id"])
|
||||||
roleLabel := ""
|
roleLabel := ""
|
||||||
teamConfig := GetTeamConfig(locale)
|
teamConfig := GetTeamConfig(locale)
|
||||||
if teamConfig != nil && teamConfig.Roles != nil {
|
if teamConfig != nil && teamConfig.Roles != nil {
|
||||||
|
|
@ -1009,7 +1010,7 @@ func teamInvitationGetPublic(ctx context.Context, invitationID, locale string) (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process team_logo if it's a wrapper - use Data URI format for direct display in img src
|
// Process team_logo if it's a wrapper - use Data URI format for direct display in img src
|
||||||
teamLogo := toString(team["logo"])
|
teamLogo := utils.ToString(team["logo"])
|
||||||
if teamLogo != "" {
|
if teamLogo != "" {
|
||||||
teamLogo = attachment.Base64(ctx, teamLogo, true)
|
teamLogo = attachment.Base64(ctx, teamLogo, true)
|
||||||
}
|
}
|
||||||
|
|
@ -1021,15 +1022,15 @@ func teamInvitationGetPublic(ctx context.Context, invitationID, locale string) (
|
||||||
|
|
||||||
// Build public response (exclude sensitive data like IDs)
|
// Build public response (exclude sensitive data like IDs)
|
||||||
publicResponse := &PublicInvitationResponse{
|
publicResponse := &PublicInvitationResponse{
|
||||||
InvitationID: toString(invitationData["invitation_id"]),
|
InvitationID: utils.ToString(invitationData["invitation_id"]),
|
||||||
TeamName: toString(team["name"]),
|
TeamName: utils.ToString(team["name"]),
|
||||||
TeamLogo: teamLogo,
|
TeamLogo: teamLogo,
|
||||||
TeamDescription: toString(team["description"]),
|
TeamDescription: utils.ToString(team["description"]),
|
||||||
RoleLabel: roleLabel,
|
RoleLabel: roleLabel,
|
||||||
Status: toString(invitationData["status"]),
|
Status: utils.ToString(invitationData["status"]),
|
||||||
InvitedAt: toTimeString(invitationData["invited_at"]),
|
InvitedAt: utils.ToTimeString(invitationData["invited_at"]),
|
||||||
InvitationExpiresAt: toTimeString(invitationData["invitation_expires_at"]),
|
InvitationExpiresAt: utils.ToTimeString(invitationData["invitation_expires_at"]),
|
||||||
Message: toString(invitationData["message"]),
|
Message: utils.ToString(invitationData["message"]),
|
||||||
InviterInfo: inviterInfo,
|
InviterInfo: inviterInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1062,12 +1063,12 @@ func teamInvitationGet(ctx context.Context, userID, teamID, invitationID string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify invitation belongs to this team
|
// Verify invitation belongs to this team
|
||||||
if toString(invitationData["team_id"]) != teamID {
|
if utils.ToString(invitationData["team_id"]) != teamID {
|
||||||
return nil, fmt.Errorf("invitation not found in this team")
|
return nil, fmt.Errorf("invitation not found in this team")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only return if it's a pending invitation
|
// Only return if it's a pending invitation
|
||||||
if toString(invitationData["status"]) != "pending" {
|
if utils.ToString(invitationData["status"]) != "pending" {
|
||||||
return nil, fmt.Errorf("invitation not found or no longer pending")
|
return nil, fmt.Errorf("invitation not found or no longer pending")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1108,7 +1109,7 @@ func teamInvitationCreate(ctx context.Context, userID, teamID string, invitation
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get team information: %w", err)
|
return "", fmt.Errorf("failed to get team information: %w", err)
|
||||||
}
|
}
|
||||||
teamName := toString(team["name"])
|
teamName := utils.ToString(team["name"])
|
||||||
|
|
||||||
// Get inviter information for email template
|
// Get inviter information for email template
|
||||||
inviter, err := provider.GetUser(ctx, userID)
|
inviter, err := provider.GetUser(ctx, userID)
|
||||||
|
|
@ -1116,9 +1117,9 @@ func teamInvitationCreate(ctx context.Context, userID, teamID string, invitation
|
||||||
log.Warn("Failed to get inviter information: %v", err)
|
log.Warn("Failed to get inviter information: %v", err)
|
||||||
inviter = maps.MapStrAny{"name": "Team Admin"}
|
inviter = maps.MapStrAny{"name": "Team Admin"}
|
||||||
}
|
}
|
||||||
inviterName := toString(inviter["name"])
|
inviterName := utils.ToString(inviter["name"])
|
||||||
if inviterName == "" {
|
if inviterName == "" {
|
||||||
inviterName = toString(inviter["email"])
|
inviterName = utils.ToString(inviter["email"])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is already a member or has pending invitation (if user_id is provided)
|
// Check if user is already a member or has pending invitation (if user_id is provided)
|
||||||
|
|
@ -1126,10 +1127,10 @@ func teamInvitationCreate(ctx context.Context, userID, teamID string, invitation
|
||||||
var inviteeEmail string
|
var inviteeEmail string
|
||||||
|
|
||||||
// Get email from invitation data first
|
// Get email from invitation data first
|
||||||
inviteeEmail = toString(invitationData["email"])
|
inviteeEmail = utils.ToString(invitationData["email"])
|
||||||
|
|
||||||
if invitationData["user_id"] != nil && invitationData["user_id"] != "" {
|
if invitationData["user_id"] != nil && invitationData["user_id"] != "" {
|
||||||
inviteeUserID = toString(invitationData["user_id"])
|
inviteeUserID = utils.ToString(invitationData["user_id"])
|
||||||
exists, err := provider.MemberExists(ctx, teamID, inviteeUserID)
|
exists, err := provider.MemberExists(ctx, teamID, inviteeUserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to check member existence: %w", err)
|
return "", fmt.Errorf("failed to check member existence: %w", err)
|
||||||
|
|
@ -1144,7 +1145,7 @@ func teamInvitationCreate(ctx context.Context, userID, teamID string, invitation
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get user information: %w", err)
|
return "", fmt.Errorf("failed to get user information: %w", err)
|
||||||
}
|
}
|
||||||
inviteeEmail = toString(user["email"])
|
inviteeEmail = utils.ToString(user["email"])
|
||||||
|
|
||||||
// Update invitation data with email from user profile
|
// Update invitation data with email from user profile
|
||||||
if inviteeEmail != "" {
|
if inviteeEmail != "" {
|
||||||
|
|
@ -1163,7 +1164,7 @@ func teamInvitationCreate(ctx context.Context, userID, teamID string, invitation
|
||||||
shouldSendEmail = settings.SendEmail
|
shouldSendEmail = settings.SendEmail
|
||||||
} else if settingsMap, ok := invitationData["settings"].(map[string]interface{}); ok {
|
} else if settingsMap, ok := invitationData["settings"].(map[string]interface{}); ok {
|
||||||
// Fallback for map format (for backward compatibility)
|
// Fallback for map format (for backward compatibility)
|
||||||
shouldSendEmail = toBool(settingsMap["send_email"])
|
shouldSendEmail = utils.ToBool(settingsMap["send_email"])
|
||||||
}
|
}
|
||||||
|
|
||||||
// If send_email is true, email must be provided
|
// If send_email is true, email must be provided
|
||||||
|
|
@ -1184,7 +1185,7 @@ func teamInvitationCreate(ctx context.Context, userID, teamID string, invitation
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save request_base_url and settings before database operation (they will be lost in DB)
|
// Save request_base_url and settings before database operation (they will be lost in DB)
|
||||||
requestBaseURL := toString(invitationData["request_base_url"])
|
requestBaseURL := utils.ToString(invitationData["request_base_url"])
|
||||||
savedSettings := invitationData["settings"] // Save settings reference
|
savedSettings := invitationData["settings"] // Save settings reference
|
||||||
|
|
||||||
// Set invitation-specific fields
|
// Set invitation-specific fields
|
||||||
|
|
@ -1213,7 +1214,7 @@ func teamInvitationCreate(ctx context.Context, userID, teamID string, invitation
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the generated invitation_id
|
// Get the generated invitation_id
|
||||||
invitationID := toString(createdMember["invitation_id"])
|
invitationID := utils.ToString(createdMember["invitation_id"])
|
||||||
|
|
||||||
// Send email if requested (shouldSendEmail was already determined earlier)
|
// Send email if requested (shouldSendEmail was already determined earlier)
|
||||||
if shouldSendEmail {
|
if shouldSendEmail {
|
||||||
|
|
@ -1270,17 +1271,17 @@ func teamInvitationResend(ctx context.Context, userID, teamID, invitationID, req
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify invitation belongs to this team
|
// Verify invitation belongs to this team
|
||||||
if toString(invitationData["team_id"]) != teamID {
|
if utils.ToString(invitationData["team_id"]) != teamID {
|
||||||
return fmt.Errorf("invitation not found in this team")
|
return fmt.Errorf("invitation not found in this team")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if invitation is still pending
|
// Check if invitation is still pending
|
||||||
if toString(invitationData["status"]) != "pending" {
|
if utils.ToString(invitationData["status"]) != "pending" {
|
||||||
return fmt.Errorf("invitation is no longer pending and cannot be resent")
|
return fmt.Errorf("invitation is no longer pending and cannot be resent")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get email directly from member record's email field
|
// Get email directly from member record's email field
|
||||||
inviteeEmail := toString(invitationData["email"])
|
inviteeEmail := utils.ToString(invitationData["email"])
|
||||||
if inviteeEmail == "" {
|
if inviteeEmail == "" {
|
||||||
return fmt.Errorf("invitation has no email address, cannot resend")
|
return fmt.Errorf("invitation has no email address, cannot resend")
|
||||||
}
|
}
|
||||||
|
|
@ -1290,7 +1291,7 @@ func teamInvitationResend(ctx context.Context, userID, teamID, invitationID, req
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get team information: %w", err)
|
return fmt.Errorf("failed to get team information: %w", err)
|
||||||
}
|
}
|
||||||
teamName := toString(team["name"])
|
teamName := utils.ToString(team["name"])
|
||||||
|
|
||||||
// Get inviter information for email template
|
// Get inviter information for email template
|
||||||
inviter, err := provider.GetUser(ctx, userID)
|
inviter, err := provider.GetUser(ctx, userID)
|
||||||
|
|
@ -1298,9 +1299,9 @@ func teamInvitationResend(ctx context.Context, userID, teamID, invitationID, req
|
||||||
log.Warn("Failed to get inviter information: %v", err)
|
log.Warn("Failed to get inviter information: %v", err)
|
||||||
inviter = maps.MapStrAny{"name": "Team Admin"}
|
inviter = maps.MapStrAny{"name": "Team Admin"}
|
||||||
}
|
}
|
||||||
inviterName := toString(inviter["name"])
|
inviterName := utils.ToString(inviter["name"])
|
||||||
if inviterName == "" {
|
if inviterName == "" {
|
||||||
inviterName = toString(inviter["email"])
|
inviterName = utils.ToString(inviter["email"])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new invitation token
|
// Generate new invitation token
|
||||||
|
|
@ -1402,12 +1403,12 @@ func teamInvitationDelete(ctx context.Context, userID, teamID, invitationID stri
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify invitation belongs to this team
|
// Verify invitation belongs to this team
|
||||||
if toString(invitationData["team_id"]) != teamID {
|
if utils.ToString(invitationData["team_id"]) != teamID {
|
||||||
return fmt.Errorf("invitation not found in this team")
|
return fmt.Errorf("invitation not found in this team")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if invitation is still pending
|
// Check if invitation is still pending
|
||||||
if toString(invitationData["status"]) != "pending" {
|
if utils.ToString(invitationData["status"]) != "pending" {
|
||||||
return fmt.Errorf("invitation is no longer pending and cannot be cancelled")
|
return fmt.Errorf("invitation is no longer pending and cannot be cancelled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1440,7 +1441,7 @@ func getTeamInvitationExpiry(invitationData maps.MapStrAny) (time.Duration, erro
|
||||||
defaultExpiry := 7 * 24 * time.Hour
|
defaultExpiry := 7 * 24 * time.Hour
|
||||||
|
|
||||||
// Check if expiry is provided in request
|
// Check if expiry is provided in request
|
||||||
expiry := toString(invitationData["expiry"])
|
expiry := utils.ToString(invitationData["expiry"])
|
||||||
if expiry != "" {
|
if expiry != "" {
|
||||||
normalizedDuration, err := normalizeDuration(expiry)
|
normalizedDuration, err := normalizeDuration(expiry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1462,7 +1463,7 @@ func getTeamInvitationExpiry(invitationData maps.MapStrAny) (time.Duration, erro
|
||||||
}
|
}
|
||||||
} else if settingsMap, ok := invitationData["settings"].(map[string]interface{}); ok {
|
} else if settingsMap, ok := invitationData["settings"].(map[string]interface{}); ok {
|
||||||
// Fallback for map format (for backward compatibility)
|
// Fallback for map format (for backward compatibility)
|
||||||
if loc := toString(settingsMap["locale"]); loc != "" {
|
if loc := utils.ToString(settingsMap["locale"]); loc != "" {
|
||||||
locale = loc
|
locale = loc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1500,7 +1501,7 @@ func sendTeamInvitationEmail(ctx context.Context, email, inviterName, teamName,
|
||||||
}
|
}
|
||||||
} else if settingsMap, ok := invitationData["settings"].(map[string]interface{}); ok {
|
} else if settingsMap, ok := invitationData["settings"].(map[string]interface{}); ok {
|
||||||
// Fallback for map format (for backward compatibility)
|
// Fallback for map format (for backward compatibility)
|
||||||
if loc := toString(settingsMap["locale"]); loc != "" {
|
if loc := utils.ToString(settingsMap["locale"]); loc != "" {
|
||||||
locale = loc
|
locale = loc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1530,19 +1531,19 @@ func sendTeamInvitationEmail(ctx context.Context, email, inviterName, teamName,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get custom message from invitation data
|
// Get custom message from invitation data
|
||||||
customMessage := toString(invitationData["message"])
|
customMessage := utils.ToString(invitationData["message"])
|
||||||
|
|
||||||
// Get request base URL from invitation data (if provided)
|
// Get request base URL from invitation data (if provided)
|
||||||
requestBaseURL := toString(invitationData["request_base_url"])
|
requestBaseURL := utils.ToString(invitationData["request_base_url"])
|
||||||
|
|
||||||
// Build invitation link using centralized helper function
|
// Build invitation link using centralized helper function
|
||||||
invitationLink := buildTeamInvitationLink(invitationID, token, teamConfig, requestBaseURL)
|
invitationLink := buildTeamInvitationLink(invitationID, token, teamConfig, requestBaseURL)
|
||||||
|
|
||||||
// Get time format based on locale
|
// Get time format based on locale
|
||||||
timeFormat := getTimeFormat(locale)
|
timeFormat := utils.GetTimeFormat(locale)
|
||||||
|
|
||||||
// Format expires_at with locale-specific format
|
// Format expires_at with locale-specific format
|
||||||
expiresAtFormatted := formatTimeWithLocale(invitationData["invitation_expires_at"], timeFormat)
|
expiresAtFormatted := utils.FormatTimeWithLocale(invitationData["invitation_expires_at"], timeFormat)
|
||||||
|
|
||||||
// Prepare template data for messenger
|
// Prepare template data for messenger
|
||||||
templateData := messengertypes.TemplateData{
|
templateData := messengertypes.TemplateData{
|
||||||
|
|
@ -1553,7 +1554,7 @@ func sendTeamInvitationEmail(ctx context.Context, email, inviterName, teamName,
|
||||||
"invitation_link": invitationLink, // Full invitation link
|
"invitation_link": invitationLink, // Full invitation link
|
||||||
"token": token, // Keep token for backward compatibility
|
"token": token, // Keep token for backward compatibility
|
||||||
"message": customMessage,
|
"message": customMessage,
|
||||||
"role_id": toString(invitationData["role_id"]),
|
"role_id": utils.ToString(invitationData["role_id"]),
|
||||||
"expires_at": expiresAtFormatted,
|
"expires_at": expiresAtFormatted,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1574,20 +1575,20 @@ func convertToTeamInvitationResponse(data maps.MapStrAny, requestBaseURL string)
|
||||||
// mapToTeamInvitationResponse converts a map to InvitationResponse
|
// mapToTeamInvitationResponse converts a map to InvitationResponse
|
||||||
func mapToTeamInvitationResponse(data maps.MapStr, requestBaseURL string) InvitationResponse {
|
func mapToTeamInvitationResponse(data maps.MapStr, requestBaseURL string) InvitationResponse {
|
||||||
invitation := InvitationResponse{
|
invitation := InvitationResponse{
|
||||||
ID: toInt64(data["id"]),
|
ID: utils.ToInt64(data["id"]),
|
||||||
InvitationID: toString(data["invitation_id"]),
|
InvitationID: utils.ToString(data["invitation_id"]),
|
||||||
TeamID: toString(data["team_id"]),
|
TeamID: utils.ToString(data["team_id"]),
|
||||||
UserID: toString(data["user_id"]),
|
UserID: utils.ToString(data["user_id"]),
|
||||||
MemberType: toString(data["member_type"]),
|
MemberType: utils.ToString(data["member_type"]),
|
||||||
RoleID: toString(data["role_id"]),
|
RoleID: utils.ToString(data["role_id"]),
|
||||||
Status: toString(data["status"]),
|
Status: utils.ToString(data["status"]),
|
||||||
InvitedBy: toString(data["invited_by"]),
|
InvitedBy: utils.ToString(data["invited_by"]),
|
||||||
InvitedAt: toTimeString(data["invited_at"]),
|
InvitedAt: utils.ToTimeString(data["invited_at"]),
|
||||||
InvitationToken: toString(data["invitation_token"]),
|
InvitationToken: utils.ToString(data["invitation_token"]),
|
||||||
InvitationExpiresAt: toTimeString(data["invitation_expires_at"]),
|
InvitationExpiresAt: utils.ToTimeString(data["invitation_expires_at"]),
|
||||||
Message: toString(data["message"]),
|
Message: utils.ToString(data["message"]),
|
||||||
CreatedAt: toTimeString(data["created_at"]),
|
CreatedAt: utils.ToTimeString(data["created_at"]),
|
||||||
UpdatedAt: toTimeString(data["updated_at"]),
|
UpdatedAt: utils.ToTimeString(data["updated_at"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add settings if available
|
// Add settings if available
|
||||||
|
|
@ -1601,8 +1602,8 @@ func mapToTeamInvitationResponse(data maps.MapStr, requestBaseURL string) Invita
|
||||||
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
||||||
// Convert map to InvitationSettings
|
// Convert map to InvitationSettings
|
||||||
invSettings := &InvitationSettings{
|
invSettings := &InvitationSettings{
|
||||||
SendEmail: toBool(settingsMap["send_email"]),
|
SendEmail: utils.ToBool(settingsMap["send_email"]),
|
||||||
Locale: toString(settingsMap["locale"]),
|
Locale: utils.ToString(settingsMap["locale"]),
|
||||||
}
|
}
|
||||||
invitation.Settings = invSettings
|
invitation.Settings = invSettings
|
||||||
if invSettings.Locale != "" {
|
if invSettings.Locale != "" {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/yaoapp/gou/process"
|
"github.com/yaoapp/gou/process"
|
||||||
|
|
@ -31,274 +28,6 @@ func GetUserIDFromSession(process *process.Process) string {
|
||||||
return userIDStr
|
return userIDStr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type Conversion Utilities
|
|
||||||
|
|
||||||
// toBool converts various types to boolean
|
|
||||||
// Supports: bool, int, int64, float64, string
|
|
||||||
// String values: "true", "false", "1", "0", "enabled", "disabled", "yes", "no", "on", "off"
|
|
||||||
// Returns false for nil or unsupported types
|
|
||||||
func toBool(v interface{}) bool {
|
|
||||||
if v == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val := v.(type) {
|
|
||||||
case bool:
|
|
||||||
return val
|
|
||||||
case int:
|
|
||||||
return val != 0
|
|
||||||
case int64:
|
|
||||||
return val != 0
|
|
||||||
case float64:
|
|
||||||
return val != 0
|
|
||||||
case string:
|
|
||||||
// Normalize string to lowercase for case-insensitive comparison
|
|
||||||
normalized := strings.ToLower(strings.TrimSpace(val))
|
|
||||||
switch normalized {
|
|
||||||
case "true", "1", "enabled", "yes", "on":
|
|
||||||
return true
|
|
||||||
case "false", "0", "disabled", "no", "off", "":
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// toString converts various types to string
|
|
||||||
// Supports: string, int, int64, float64, bool, time.Time, *time.Time
|
|
||||||
// time.Time is formatted using the optional timeFormat parameter
|
|
||||||
// If timeFormat is not provided, defaults to "2006-01-02 15:04:05"
|
|
||||||
// Returns empty string for nil or unsupported types
|
|
||||||
func toString(v interface{}, timeFormat ...string) string {
|
|
||||||
if v == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get time format (default or provided)
|
|
||||||
format := "2006-01-02 15:04:05"
|
|
||||||
if len(timeFormat) > 0 && timeFormat[0] != "" {
|
|
||||||
format = timeFormat[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val := v.(type) {
|
|
||||||
case string:
|
|
||||||
return val
|
|
||||||
case int:
|
|
||||||
return fmt.Sprintf("%d", val)
|
|
||||||
case int64:
|
|
||||||
return fmt.Sprintf("%d", val)
|
|
||||||
case float64:
|
|
||||||
return fmt.Sprintf("%.0f", val)
|
|
||||||
case bool:
|
|
||||||
if val {
|
|
||||||
return "true"
|
|
||||||
}
|
|
||||||
return "false"
|
|
||||||
case time.Time:
|
|
||||||
return val.Format(format)
|
|
||||||
case *time.Time:
|
|
||||||
if val != nil {
|
|
||||||
return val.Format(format)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getTimeFormat returns the appropriate time format string for the given locale
|
|
||||||
// Returns format suitable for time.Format()
|
|
||||||
func getTimeFormat(locale string) string {
|
|
||||||
// Normalize locale to lowercase
|
|
||||||
locale = strings.ToLower(strings.TrimSpace(locale))
|
|
||||||
|
|
||||||
switch locale {
|
|
||||||
case "zh-cn", "zh":
|
|
||||||
// Chinese format: 2025年10月30日 08:57:51
|
|
||||||
return "2006年01月02日 15:04:05"
|
|
||||||
case "en", "en-us", "":
|
|
||||||
// English format: October 30, 2025 08:57:51
|
|
||||||
return "January 02, 2006 15:04:05"
|
|
||||||
default:
|
|
||||||
// Default ISO format
|
|
||||||
return "2006-01-02 15:04:05"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatTimeWithLocale formats a time value (time.Time, *time.Time, or string) using the specified format
|
|
||||||
// If the input is already a string, it will parse it first and then reformat it
|
|
||||||
// Returns empty string if the value cannot be parsed
|
|
||||||
func formatTimeWithLocale(v interface{}, targetFormat string) string {
|
|
||||||
if v == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var t time.Time
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch val := v.(type) {
|
|
||||||
case time.Time:
|
|
||||||
t = val
|
|
||||||
case *time.Time:
|
|
||||||
if val != nil {
|
|
||||||
t = *val
|
|
||||||
} else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
case string:
|
|
||||||
// Try parsing with common formats
|
|
||||||
formats := []string{
|
|
||||||
"2006-01-02 15:04:05",
|
|
||||||
"2006-01-02T15:04:05Z",
|
|
||||||
"2006-01-02T15:04:05",
|
|
||||||
time.RFC3339,
|
|
||||||
}
|
|
||||||
for _, format := range formats {
|
|
||||||
t, err = time.Parse(format, val)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// If all parsing attempts failed, return the original string
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// For unsupported types, try toString first
|
|
||||||
str := toString(v)
|
|
||||||
if str == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
// Try parsing the string
|
|
||||||
return formatTimeWithLocale(str, targetFormat)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format with target format
|
|
||||||
return t.Format(targetFormat)
|
|
||||||
}
|
|
||||||
|
|
||||||
// toInt64 converts various types to int64
|
|
||||||
// Supports: int, int64, float64, string
|
|
||||||
// Returns 0 for nil or unsupported types
|
|
||||||
func toInt64(v interface{}) int64 {
|
|
||||||
if v == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val := v.(type) {
|
|
||||||
case int64:
|
|
||||||
return val
|
|
||||||
case int:
|
|
||||||
return int64(val)
|
|
||||||
case float64:
|
|
||||||
return int64(val)
|
|
||||||
case string:
|
|
||||||
if parsed, err := strconv.ParseInt(val, 10, 64); err == nil {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// toTimeString converts various time types to RFC3339 string
|
|
||||||
// Supports: time.Time, string, int64 (unix timestamp)
|
|
||||||
// Returns empty string for nil or unsupported types
|
|
||||||
func toTimeString(v interface{}) string {
|
|
||||||
if v == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val := v.(type) {
|
|
||||||
case time.Time:
|
|
||||||
if val.IsZero() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return val.Format(time.RFC3339)
|
|
||||||
case string:
|
|
||||||
// Try to parse as RFC3339 first
|
|
||||||
if t, err := time.Parse(time.RFC3339, val); err == nil {
|
|
||||||
return t.Format(time.RFC3339)
|
|
||||||
}
|
|
||||||
// Try to parse as other common formats
|
|
||||||
formats := []string{
|
|
||||||
"2006-01-02 15:04:05",
|
|
||||||
"2006-01-02T15:04:05Z",
|
|
||||||
"2006-01-02T15:04:05.000Z",
|
|
||||||
}
|
|
||||||
for _, format := range formats {
|
|
||||||
if t, err := time.Parse(format, val); err == nil {
|
|
||||||
return t.Format(time.RFC3339)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return val // Return as-is if can't parse
|
|
||||||
case int64:
|
|
||||||
// Assume unix timestamp
|
|
||||||
if val > 0 {
|
|
||||||
return time.Unix(val, 0).Format(time.RFC3339)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// toInt converts various types to int
|
|
||||||
// Supports: int, int64, float64, string
|
|
||||||
// Returns 0 for nil or unsupported types
|
|
||||||
func toInt(v interface{}) int {
|
|
||||||
if v == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val := v.(type) {
|
|
||||||
case int:
|
|
||||||
return val
|
|
||||||
case int64:
|
|
||||||
return int(val)
|
|
||||||
case float64:
|
|
||||||
return int(val)
|
|
||||||
case string:
|
|
||||||
if parsed, err := strconv.Atoi(val); err == nil {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// toFloat64 converts various types to float64
|
|
||||||
// Supports: float64, int, int64, string
|
|
||||||
// Returns 0.0 for nil or unsupported types
|
|
||||||
func toFloat64(v interface{}) float64 {
|
|
||||||
if v == nil {
|
|
||||||
return 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val := v.(type) {
|
|
||||||
case float64:
|
|
||||||
return val
|
|
||||||
case float32:
|
|
||||||
return float64(val)
|
|
||||||
case int:
|
|
||||||
return float64(val)
|
|
||||||
case int64:
|
|
||||||
return float64(val)
|
|
||||||
case string:
|
|
||||||
if parsed, err := strconv.ParseFloat(val, 64); err == nil {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
return 0.0
|
|
||||||
default:
|
|
||||||
return 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Security Utilities
|
// Security Utilities
|
||||||
|
|
||||||
// maskEmail masks an email address for privacy protection
|
// maskEmail masks an email address for privacy protection
|
||||||
|
|
|
||||||
277
openapi/utils/convert.go
Normal file
277
openapi/utils/convert.go
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Type Conversion Utilities
|
||||||
|
// These functions provide safe type conversion from interface{} to common types
|
||||||
|
|
||||||
|
// ToBool converts various types to boolean
|
||||||
|
// Supports: bool, int, int64, float64, string
|
||||||
|
// String values: "true", "false", "1", "0", "enabled", "disabled", "yes", "no", "on", "off"
|
||||||
|
// Returns false for nil or unsupported types
|
||||||
|
func ToBool(v interface{}) bool {
|
||||||
|
if v == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return val
|
||||||
|
case int:
|
||||||
|
return val != 0
|
||||||
|
case int64:
|
||||||
|
return val != 0
|
||||||
|
case float64:
|
||||||
|
return val != 0
|
||||||
|
case string:
|
||||||
|
// Normalize string to lowercase for case-insensitive comparison
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(val))
|
||||||
|
switch normalized {
|
||||||
|
case "true", "1", "enabled", "yes", "on":
|
||||||
|
return true
|
||||||
|
case "false", "0", "disabled", "no", "off", "":
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToString converts various types to string
|
||||||
|
// Supports: string, int, int64, float64, bool, time.Time, *time.Time
|
||||||
|
// time.Time is formatted using the optional timeFormat parameter
|
||||||
|
// If timeFormat is not provided, defaults to "2006-01-02 15:04:05"
|
||||||
|
// Returns empty string for nil or unsupported types
|
||||||
|
func ToString(v interface{}, timeFormat ...string) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get time format (default or provided)
|
||||||
|
format := "2006-01-02 15:04:05"
|
||||||
|
if len(timeFormat) > 0 && timeFormat[0] != "" {
|
||||||
|
format = timeFormat[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := v.(type) {
|
||||||
|
case string:
|
||||||
|
return val
|
||||||
|
case int:
|
||||||
|
return fmt.Sprintf("%d", val)
|
||||||
|
case int64:
|
||||||
|
return fmt.Sprintf("%d", val)
|
||||||
|
case float64:
|
||||||
|
return fmt.Sprintf("%.0f", val)
|
||||||
|
case bool:
|
||||||
|
if val {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return "false"
|
||||||
|
case time.Time:
|
||||||
|
return val.Format(format)
|
||||||
|
case *time.Time:
|
||||||
|
if val != nil {
|
||||||
|
return val.Format(format)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToInt64 converts various types to int64
|
||||||
|
// Supports: int, int64, float64, string
|
||||||
|
// Returns 0 for nil or unsupported types
|
||||||
|
func ToInt64(v interface{}) int64 {
|
||||||
|
if v == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := v.(type) {
|
||||||
|
case int64:
|
||||||
|
return val
|
||||||
|
case int:
|
||||||
|
return int64(val)
|
||||||
|
case float64:
|
||||||
|
return int64(val)
|
||||||
|
case string:
|
||||||
|
if parsed, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToInt converts various types to int
|
||||||
|
// Supports: int, int64, float64, string
|
||||||
|
// Returns 0 for nil or unsupported types
|
||||||
|
func ToInt(v interface{}) int {
|
||||||
|
if v == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := v.(type) {
|
||||||
|
case int:
|
||||||
|
return val
|
||||||
|
case int64:
|
||||||
|
return int(val)
|
||||||
|
case float64:
|
||||||
|
return int(val)
|
||||||
|
case string:
|
||||||
|
if parsed, err := strconv.Atoi(val); err == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFloat64 converts various types to float64
|
||||||
|
// Supports: float64, int, int64, string
|
||||||
|
// Returns 0.0 for nil or unsupported types
|
||||||
|
func ToFloat64(v interface{}) float64 {
|
||||||
|
if v == nil {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return val
|
||||||
|
case float32:
|
||||||
|
return float64(val)
|
||||||
|
case int:
|
||||||
|
return float64(val)
|
||||||
|
case int64:
|
||||||
|
return float64(val)
|
||||||
|
case string:
|
||||||
|
if parsed, err := strconv.ParseFloat(val, 64); err == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
default:
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToTimeString converts various time types to RFC3339 string
|
||||||
|
// Supports: time.Time, string, int64 (unix timestamp)
|
||||||
|
// Returns empty string for nil or unsupported types
|
||||||
|
func ToTimeString(v interface{}) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := v.(type) {
|
||||||
|
case time.Time:
|
||||||
|
if val.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.Format(time.RFC3339)
|
||||||
|
case string:
|
||||||
|
// Try to parse as RFC3339 first
|
||||||
|
if t, err := time.Parse(time.RFC3339, val); err == nil {
|
||||||
|
return t.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
// Try to parse as other common formats
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
"2006-01-02T15:04:05Z",
|
||||||
|
"2006-01-02T15:04:05.000Z",
|
||||||
|
}
|
||||||
|
for _, format := range formats {
|
||||||
|
if t, err := time.Parse(format, val); err == nil {
|
||||||
|
return t.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val // Return as-is if can't parse
|
||||||
|
case int64:
|
||||||
|
// Assume unix timestamp
|
||||||
|
if val > 0 {
|
||||||
|
return time.Unix(val, 0).Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeFormat returns the appropriate time format string for the given locale
|
||||||
|
// Returns format suitable for time.Format()
|
||||||
|
func GetTimeFormat(locale string) string {
|
||||||
|
// Normalize locale to lowercase
|
||||||
|
locale = strings.ToLower(strings.TrimSpace(locale))
|
||||||
|
|
||||||
|
switch locale {
|
||||||
|
case "zh-cn", "zh":
|
||||||
|
// Chinese format: 2025年10月30日 08:57:51
|
||||||
|
return "2006年01月02日 15:04:05"
|
||||||
|
case "en", "en-us", "":
|
||||||
|
// English format: October 30, 2025 08:57:51
|
||||||
|
return "January 02, 2006 15:04:05"
|
||||||
|
default:
|
||||||
|
// Default ISO format
|
||||||
|
return "2006-01-02 15:04:05"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatTimeWithLocale formats a time value (time.Time, *time.Time, or string) using the specified format
|
||||||
|
// If the input is already a string, it will parse it first and then reformat it
|
||||||
|
// Returns empty string if the value cannot be parsed
|
||||||
|
func FormatTimeWithLocale(v interface{}, targetFormat string) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var t time.Time
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch val := v.(type) {
|
||||||
|
case time.Time:
|
||||||
|
t = val
|
||||||
|
case *time.Time:
|
||||||
|
if val != nil {
|
||||||
|
t = *val
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
// Try parsing with common formats
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
"2006-01-02T15:04:05Z",
|
||||||
|
"2006-01-02T15:04:05",
|
||||||
|
time.RFC3339,
|
||||||
|
}
|
||||||
|
for _, format := range formats {
|
||||||
|
t, err = time.Parse(format, val)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// If all parsing attempts failed, return the original string
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// For unsupported types, try ToString first
|
||||||
|
str := ToString(v)
|
||||||
|
if str == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Try parsing the string
|
||||||
|
return FormatTimeWithLocale(str, targetFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format with target format
|
||||||
|
return t.Format(targetFormat)
|
||||||
|
}
|
||||||
|
|
@ -62,22 +62,34 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "system",
|
"name": "preset",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "System Collection",
|
"label": "Preset Collection",
|
||||||
"comment": "Whether this is a system collection",
|
"comment": "Whether this is a preset collection",
|
||||||
"default": false,
|
"default": false,
|
||||||
"nullable": false
|
"nullable": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "readonly",
|
"name": "public",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Readonly Collection",
|
"label": "Public Collection",
|
||||||
"comment": "Whether this collection is read-only",
|
"comment": "Whether this collection is shared across all teams in the platform",
|
||||||
"index": true,
|
|
||||||
"default": false,
|
"default": false,
|
||||||
"nullable": false
|
"nullable": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "share",
|
||||||
|
"type": "enum",
|
||||||
|
"label": "Share",
|
||||||
|
"comment": "Collection sharing scope",
|
||||||
|
"option": [
|
||||||
|
"private", // Only visible to the owner
|
||||||
|
"team" // Visible to all team members
|
||||||
|
],
|
||||||
|
"default": "private",
|
||||||
|
"nullable": false,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "sort",
|
"name": "sort",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
@ -193,8 +205,5 @@
|
||||||
"default": 10
|
"default": 10
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"option": {
|
"option": { "soft_deletes": true, "permission": true, "timestamps": true }
|
||||||
"permission": true,
|
|
||||||
"timestamps": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,21 +133,34 @@
|
||||||
"default": "en"
|
"default": "en"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "system",
|
"name": "preset",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "System Document",
|
"label": "Preset Document",
|
||||||
"comment": "Whether this is a system document",
|
"comment": "Whether this is a preset document",
|
||||||
"default": false,
|
"default": false,
|
||||||
"nullable": false
|
"nullable": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "readonly",
|
"name": "public",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Readonly Document",
|
"label": "Public Document",
|
||||||
"comment": "Whether this document is read-only",
|
"comment": "Whether this document is shared across all teams in the platform",
|
||||||
"default": false,
|
"default": false,
|
||||||
"nullable": false
|
"nullable": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "share",
|
||||||
|
"type": "enum",
|
||||||
|
"label": "Share",
|
||||||
|
"comment": "Document sharing scope",
|
||||||
|
"option": [
|
||||||
|
"private", // Only visible to the owner
|
||||||
|
"team" // Visible to all team members
|
||||||
|
],
|
||||||
|
"default": "private",
|
||||||
|
"nullable": false,
|
||||||
|
"index": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "sort",
|
"name": "sort",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|
@ -352,8 +365,5 @@
|
||||||
"nullable": true
|
"nullable": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"option": {
|
"option": { "soft_deletes": true, "permission": true, "timestamps": true }
|
||||||
"permission": true,
|
|
||||||
"timestamps": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue