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/maps"
|
||||
"github.com/yaoapp/yao/kb"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
)
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ var (
|
|||
// availableCollectionFields defines all available fields for security filtering
|
||||
availableCollectionFields = map[string]bool{
|
||||
"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,
|
||||
"embedding_properties": true, "locale": true, "dimension": true,
|
||||
"distance_metric": true, "hnsw_m": true, "ef_construction": true,
|
||||
|
|
@ -32,7 +33,7 @@ var (
|
|||
|
||||
// defaultCollectionFields defines the default compact field list
|
||||
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",
|
||||
"locale", "dimension", "distance_metric", "created_at", "updated_at",
|
||||
}
|
||||
|
|
@ -57,6 +58,7 @@ type ProviderSettings struct {
|
|||
|
||||
// CreateCollection creates a new collection
|
||||
func CreateCollection(c *gin.Context) {
|
||||
|
||||
// Prepare request and database data
|
||||
req, collectionData, err := PrepareCreateCollection(c)
|
||||
if err != nil {
|
||||
|
|
@ -68,6 +70,12 @@ func CreateCollection(c *gin.Context) {
|
|||
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
|
||||
if kb.Instance == nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/yaoapp/yao/attachment"
|
||||
"github.com/yaoapp/yao/kb"
|
||||
kbtypes "github.com/yaoapp/yao/kb/types"
|
||||
apiutils "github.com/yaoapp/yao/openapi/utils"
|
||||
)
|
||||
|
||||
// PrepareCreateCollection prepares CreateCollection request and database data
|
||||
|
|
@ -70,6 +71,12 @@ func PrepareCreateCollection(c *gin.Context) (*CreateCollectionRequest, map[stri
|
|||
"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
|
||||
if req.Config.M > 0 {
|
||||
data["m"] = req.Config.M
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
||||
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
"github.com/yaoapp/yao/openapi/utils"
|
||||
"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
|
||||
mfaEnabled := toBool(user["mfa_enabled"])
|
||||
mfaEnabled := utils.ToBool(user["mfa_enabled"])
|
||||
|
||||
// If MFA enabled, generate MFA token
|
||||
if mfaEnabled {
|
||||
|
|
@ -459,7 +460,7 @@ func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse
|
|||
extraClaims["team_id"] = params.TeamID
|
||||
|
||||
// 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
|
||||
oidcUserInfo.YaoTenantID = tenantID
|
||||
}
|
||||
|
|
@ -467,21 +468,21 @@ func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse
|
|||
// Add team info to OIDC user info
|
||||
oidcUserInfo.YaoTeamID = params.TeamID
|
||||
teamInfo := &oauthtypes.OIDCTeamInfo{}
|
||||
if teamIDVal := toString(params.Team["team_id"]); teamIDVal != "" {
|
||||
if teamIDVal := utils.ToString(params.Team["team_id"]); teamIDVal != "" {
|
||||
teamInfo.TeamID = teamIDVal
|
||||
}
|
||||
if logo := toString(params.Team["logo"]); logo != "" {
|
||||
if logo := utils.ToString(params.Team["logo"]); logo != "" {
|
||||
teamInfo.Logo = logo
|
||||
}
|
||||
if name := toString(params.Team["name"]); name != "" {
|
||||
if name := utils.ToString(params.Team["name"]); name != "" {
|
||||
teamInfo.Name = name
|
||||
}
|
||||
if description := toString(params.Team["description"]); description != "" {
|
||||
if description := utils.ToString(params.Team["description"]); description != "" {
|
||||
teamInfo.Description = description
|
||||
}
|
||||
|
||||
// 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
|
||||
teamInfo.OwnerID = ownerID
|
||||
|
||||
|
|
@ -497,19 +498,19 @@ func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse
|
|||
// Add member profile information if available
|
||||
if params.Member != nil {
|
||||
memberInfo := &oauthtypes.OIDCMemberInfo{}
|
||||
if memberID := toString(params.Member["member_id"]); memberID != "" {
|
||||
if memberID := utils.ToString(params.Member["member_id"]); memberID != "" {
|
||||
memberInfo.MemberID = memberID
|
||||
}
|
||||
if displayName := toString(params.Member["display_name"]); displayName != "" {
|
||||
if displayName := utils.ToString(params.Member["display_name"]); displayName != "" {
|
||||
memberInfo.DisplayName = displayName
|
||||
}
|
||||
if bio := toString(params.Member["bio"]); bio != "" {
|
||||
if bio := utils.ToString(params.Member["bio"]); bio != "" {
|
||||
memberInfo.Bio = bio
|
||||
}
|
||||
if avatar := toString(params.Member["avatar"]); avatar != "" {
|
||||
if avatar := utils.ToString(params.Member["avatar"]); avatar != "" {
|
||||
memberInfo.Avatar = avatar
|
||||
}
|
||||
if email := toString(params.Member["email"]); email != "" {
|
||||
if email := utils.ToString(params.Member["email"]); email != "" {
|
||||
memberInfo.Email = email
|
||||
}
|
||||
oidcUserInfo.YaoMember = memberInfo
|
||||
|
|
@ -520,10 +521,10 @@ func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse
|
|||
var typeID string
|
||||
if params.TeamID != "" && params.Team != nil {
|
||||
// Team context - use team's type
|
||||
typeID = toString(params.Team["type_id"])
|
||||
typeID = utils.ToString(params.Team["type_id"])
|
||||
} else {
|
||||
// Personal context - use user's type
|
||||
typeID = toString(params.User["type_id"])
|
||||
typeID = utils.ToString(params.User["type_id"])
|
||||
}
|
||||
|
||||
if typeID != "" {
|
||||
|
|
@ -538,13 +539,13 @@ func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse
|
|||
if err == nil && typeInfo != nil {
|
||||
// Add type info to OIDC user info
|
||||
typeDetails := &oauthtypes.OIDCTypeInfo{}
|
||||
if typeIDVal := toString(typeInfo["type_id"]); typeIDVal != "" {
|
||||
if typeIDVal := utils.ToString(typeInfo["type_id"]); typeIDVal != "" {
|
||||
typeDetails.TypeID = typeIDVal
|
||||
}
|
||||
if name := toString(typeInfo["name"]); name != "" {
|
||||
if name := utils.ToString(typeInfo["name"]); name != "" {
|
||||
typeDetails.Name = name
|
||||
}
|
||||
if locale := toString(typeInfo["locale"]); locale != "" {
|
||||
if locale := utils.ToString(typeInfo["locale"]); locale != "" {
|
||||
typeDetails.Locale = locale
|
||||
}
|
||||
oidcUserInfo.YaoType = typeDetails
|
||||
|
|
@ -595,7 +596,7 @@ func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse
|
|||
ExpiresIn: expiresIn,
|
||||
RefreshTokenExpiresIn: refreshTokenExpiresIn,
|
||||
TokenType: "Bearer",
|
||||
MFAEnabled: toBool(params.User["mfa_enabled"]),
|
||||
MFAEnabled: utils.ToBool(params.User["mfa_enabled"]),
|
||||
Scope: strings.Join(params.Scopes, " "),
|
||||
Status: LoginStatusSuccess,
|
||||
}, nil
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/yaoapp/yao/openapi/oauth"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
"github.com/yaoapp/yao/openapi/utils"
|
||||
)
|
||||
|
||||
// Member Management Handlers
|
||||
|
|
@ -292,7 +293,7 @@ func GinMemberCreateRobot(c *gin.Context) {
|
|||
"bio": req.Bio,
|
||||
"role_id": req.RoleID,
|
||||
"system_prompt": req.SystemPrompt,
|
||||
"autonomous_mode": toBool(req.AutonomousMode),
|
||||
"autonomous_mode": utils.ToBool(req.AutonomousMode),
|
||||
}
|
||||
|
||||
// Add optional fields
|
||||
|
|
@ -437,7 +438,7 @@ func GinMemberUpdateRobot(c *gin.Context) {
|
|||
updateData["robot_status"] = req.RobotStatus
|
||||
}
|
||||
if req.AutonomousMode != "" {
|
||||
updateData["autonomous_mode"] = toBool(req.AutonomousMode)
|
||||
updateData["autonomous_mode"] = utils.ToBool(req.AutonomousMode)
|
||||
}
|
||||
if req.CostLimit > 0 {
|
||||
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
|
||||
func mapToMemberResponse(data maps.MapStr) MemberResponse {
|
||||
member := MemberResponse{
|
||||
ID: toInt64(data["id"]),
|
||||
MemberID: toString(data["member_id"]),
|
||||
TeamID: toString(data["team_id"]),
|
||||
UserID: toString(data["user_id"]),
|
||||
MemberType: toString(data["member_type"]),
|
||||
DisplayName: toString(data["display_name"]),
|
||||
Bio: toString(data["bio"]),
|
||||
Avatar: toString(data["avatar"]),
|
||||
Email: toString(data["email"]),
|
||||
RobotEmail: toString(data["robot_email"]), // Globally unique email for robot members
|
||||
RoleID: toString(data["role_id"]),
|
||||
ID: utils.ToInt64(data["id"]),
|
||||
MemberID: utils.ToString(data["member_id"]),
|
||||
TeamID: utils.ToString(data["team_id"]),
|
||||
UserID: utils.ToString(data["user_id"]),
|
||||
MemberType: utils.ToString(data["member_type"]),
|
||||
DisplayName: utils.ToString(data["display_name"]),
|
||||
Bio: utils.ToString(data["bio"]),
|
||||
Avatar: utils.ToString(data["avatar"]),
|
||||
Email: utils.ToString(data["email"]),
|
||||
RobotEmail: utils.ToString(data["robot_email"]), // Globally unique email for robot members
|
||||
RoleID: utils.ToString(data["role_id"]),
|
||||
IsOwner: data["is_owner"], // Keep original type (int or bool)
|
||||
Status: toString(data["status"]),
|
||||
InvitationID: toString(data["invitation_id"]),
|
||||
InvitedBy: toString(data["invited_by"]),
|
||||
InvitedAt: toTimeString(data["invited_at"]),
|
||||
InvitationToken: toString(data["invitation_token"]),
|
||||
InvitationExpiresAt: toTimeString(data["invitation_expires_at"]),
|
||||
JoinedAt: toTimeString(data["joined_at"]),
|
||||
LastActiveAt: toTimeString(data["last_active_at"]),
|
||||
LoginCount: toInt(data["login_count"]),
|
||||
CreatedAt: toTimeString(data["created_at"]),
|
||||
UpdatedAt: toTimeString(data["updated_at"]),
|
||||
Status: utils.ToString(data["status"]),
|
||||
InvitationID: utils.ToString(data["invitation_id"]),
|
||||
InvitedBy: utils.ToString(data["invited_by"]),
|
||||
InvitedAt: utils.ToTimeString(data["invited_at"]),
|
||||
InvitationToken: utils.ToString(data["invitation_token"]),
|
||||
InvitationExpiresAt: utils.ToTimeString(data["invitation_expires_at"]),
|
||||
JoinedAt: utils.ToTimeString(data["joined_at"]),
|
||||
LastActiveAt: utils.ToTimeString(data["last_active_at"]),
|
||||
LoginCount: utils.ToInt(data["login_count"]),
|
||||
CreatedAt: utils.ToTimeString(data["created_at"]),
|
||||
UpdatedAt: utils.ToTimeString(data["updated_at"]),
|
||||
}
|
||||
|
||||
// Add settings if available
|
||||
|
|
@ -1559,7 +1560,7 @@ func mapToMemberResponse(data maps.MapStr) MemberResponse {
|
|||
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
||||
// Convert map to MemberSettings (for backward compatibility)
|
||||
memSettings := &MemberSettings{
|
||||
Notifications: toBool(settingsMap["notifications"]),
|
||||
Notifications: utils.ToBool(settingsMap["notifications"]),
|
||||
}
|
||||
// Handle permissions array
|
||||
if perms, ok := settingsMap["permissions"]; ok {
|
||||
|
|
@ -1587,14 +1588,14 @@ func mapToMemberDetailResponse(data maps.MapStr) MemberDetailResponse {
|
|||
member := MemberDetailResponse{
|
||||
MemberResponse: mapToMemberResponse(data),
|
||||
// Robot-specific fields
|
||||
SystemPrompt: toString(data["system_prompt"]),
|
||||
ManagerID: toString(data["manager_id"]),
|
||||
LanguageModel: toString(data["language_model"]),
|
||||
CostLimit: toFloat64(data["cost_limit"]),
|
||||
SystemPrompt: utils.ToString(data["system_prompt"]),
|
||||
ManagerID: utils.ToString(data["manager_id"]),
|
||||
LanguageModel: utils.ToString(data["language_model"]),
|
||||
CostLimit: utils.ToFloat64(data["cost_limit"]),
|
||||
AutonomousMode: data["autonomous_mode"], // Keep original type (bool or string)
|
||||
LastRobotActivity: toTimeString(data["last_robot_activity"]),
|
||||
RobotStatus: toString(data["robot_status"]),
|
||||
Notes: toString(data["notes"]),
|
||||
LastRobotActivity: utils.ToTimeString(data["last_robot_activity"]),
|
||||
RobotStatus: utils.ToString(data["robot_status"]),
|
||||
Notes: utils.ToString(data["notes"]),
|
||||
}
|
||||
|
||||
// Handle authorized_senders array
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
||||
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
"github.com/yaoapp/yao/openapi/utils"
|
||||
)
|
||||
|
||||
// User Profile Management Handlers
|
||||
|
|
@ -246,11 +247,11 @@ func addTeamInfo(ctx context.Context, provider *user.DefaultUser, oidcUserInfo *
|
|||
if withTeam {
|
||||
oidcUserInfo.YaoTeamID = teamID
|
||||
oidcUserInfo.YaoTeam = &oauthtypes.OIDCTeamInfo{
|
||||
TeamID: toString(team["team_id"]),
|
||||
Name: toString(team["name"]),
|
||||
Description: toString(team["description"]),
|
||||
Logo: toString(team["logo"]),
|
||||
OwnerID: toString(team["owner_id"]),
|
||||
TeamID: utils.ToString(team["team_id"]),
|
||||
Name: utils.ToString(team["name"]),
|
||||
Description: utils.ToString(team["description"]),
|
||||
Logo: utils.ToString(team["logo"]),
|
||||
OwnerID: utils.ToString(team["owner_id"]),
|
||||
}
|
||||
|
||||
// Check if user is owner
|
||||
|
|
@ -260,7 +261,7 @@ func addTeamInfo(ctx context.Context, provider *user.DefaultUser, oidcUserInfo *
|
|||
}
|
||||
|
||||
// Add tenant_id if available
|
||||
if tenantID := toString(team["tenant_id"]); tenantID != "" {
|
||||
if tenantID := utils.ToString(team["tenant_id"]); tenantID != "" {
|
||||
oidcUserInfo.YaoTenantID = tenantID
|
||||
}
|
||||
}
|
||||
|
|
@ -274,13 +275,13 @@ func addTypeInfo(ctx context.Context, provider *user.DefaultUser, oidcUserInfo *
|
|||
if teamID != "" {
|
||||
team, err := provider.GetTeamByMember(ctx, teamID, userID)
|
||||
if err == nil && team != nil {
|
||||
typeID = toString(team["type_id"])
|
||||
typeID = utils.ToString(team["type_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to user's type
|
||||
if typeID == "" {
|
||||
typeID = toString(userData["type_id"])
|
||||
typeID = utils.ToString(userData["type_id"])
|
||||
}
|
||||
|
||||
if typeID == "" {
|
||||
|
|
@ -297,9 +298,9 @@ func addTypeInfo(ctx context.Context, provider *user.DefaultUser, oidcUserInfo *
|
|||
}
|
||||
|
||||
oidcUserInfo.YaoType = &oauthtypes.OIDCTypeInfo{
|
||||
TypeID: toString(typeInfo["type_id"]),
|
||||
Name: toString(typeInfo["name"]),
|
||||
Locale: toString(typeInfo["locale"]),
|
||||
TypeID: utils.ToString(typeInfo["type_id"]),
|
||||
Name: utils.ToString(typeInfo["name"]),
|
||||
Locale: utils.ToString(typeInfo["locale"]),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
"github.com/yaoapp/yao/openapi/utils"
|
||||
)
|
||||
|
||||
// Team Management Handlers
|
||||
|
|
@ -132,7 +133,7 @@ func GinTeamGet(c *gin.Context) {
|
|||
}
|
||||
|
||||
// // Check if user owns this team
|
||||
// ownerID := toString(teamData["owner_id"])
|
||||
// ownerID := utils.ToString(teamData["owner_id"])
|
||||
// if ownerID != authInfo.UserID {
|
||||
// errorResp := &response.ErrorResponse{
|
||||
// Code: response.ErrAccessDenied.Code,
|
||||
|
|
@ -878,7 +879,7 @@ func teamUpdate(ctx context.Context, userID, teamID string, updateData maps.MapS
|
|||
}
|
||||
|
||||
// Check ownership
|
||||
ownerID := toString(teamData["owner_id"])
|
||||
ownerID := utils.ToString(teamData["owner_id"])
|
||||
if ownerID != userID {
|
||||
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
|
||||
ownerID := toString(teamData["owner_id"])
|
||||
ownerID := utils.ToString(teamData["owner_id"])
|
||||
if ownerID != userID {
|
||||
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
|
||||
func mapToTeamResponse(data maps.MapStr) TeamResponse {
|
||||
team := TeamResponse{
|
||||
ID: toInt64(data["id"]),
|
||||
TeamID: toString(data["team_id"]),
|
||||
Name: toString(data["name"]),
|
||||
Description: toString(data["description"]),
|
||||
Logo: toString(data["logo"]),
|
||||
OwnerID: toString(data["owner_id"]),
|
||||
Status: toString(data["status"]),
|
||||
IsVerified: toBool(data["is_verified"]),
|
||||
VerifiedBy: toString(data["verified_by"]),
|
||||
VerifiedAt: toTimeString(data["verified_at"]),
|
||||
CreatedAt: toTimeString(data["created_at"]),
|
||||
UpdatedAt: toTimeString(data["updated_at"]),
|
||||
ID: utils.ToInt64(data["id"]),
|
||||
TeamID: utils.ToString(data["team_id"]),
|
||||
Name: utils.ToString(data["name"]),
|
||||
Description: utils.ToString(data["description"]),
|
||||
Logo: utils.ToString(data["logo"]),
|
||||
OwnerID: utils.ToString(data["owner_id"]),
|
||||
Status: utils.ToString(data["status"]),
|
||||
IsVerified: utils.ToBool(data["is_verified"]),
|
||||
VerifiedBy: utils.ToString(data["verified_by"]),
|
||||
VerifiedAt: utils.ToTimeString(data["verified_at"]),
|
||||
CreatedAt: utils.ToTimeString(data["created_at"]),
|
||||
UpdatedAt: utils.ToTimeString(data["updated_at"]),
|
||||
}
|
||||
|
||||
return team
|
||||
|
|
@ -987,8 +988,8 @@ func mapToTeamDetailResponse(data maps.MapStr) TeamDetailResponse {
|
|||
} else if settingsMap, ok := settings.(map[string]interface{}); ok {
|
||||
// Convert map to TeamSettings (for backward compatibility)
|
||||
teamSettings := &TeamSettings{
|
||||
Theme: toString(settingsMap["theme"]),
|
||||
Visibility: toString(settingsMap["visibility"]),
|
||||
Theme: utils.ToString(settingsMap["theme"]),
|
||||
Visibility: utils.ToString(settingsMap["visibility"]),
|
||||
}
|
||||
team.Settings = teamSettings
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/yaoapp/kun/maps"
|
||||
"github.com/yaoapp/yao/attachment"
|
||||
"github.com/yaoapp/yao/messenger"
|
||||
"github.com/yaoapp/yao/openapi/utils"
|
||||
messengertypes "github.com/yaoapp/yao/messenger/types"
|
||||
"github.com/yaoapp/yao/openapi/oauth"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
|
|
@ -542,7 +543,7 @@ func GinTeamInvitationAccept(c *gin.Context) {
|
|||
}
|
||||
|
||||
// Get team_id from invitation
|
||||
teamID := toString(invitationData["team_id"])
|
||||
teamID := utils.ToString(invitationData["team_id"])
|
||||
if teamID == "" {
|
||||
log.Error("Invalid invitation: missing team_id")
|
||||
errorResp := &response.ErrorResponse{
|
||||
|
|
@ -636,16 +637,16 @@ func ProcessTeamInvitationList(process *process.Process) interface{} {
|
|||
page := 1
|
||||
pagesize := 20
|
||||
|
||||
if p := int(toInt64(queryMap["page"])); p > 0 {
|
||||
if p := int(utils.ToInt64(queryMap["page"])); p > 0 {
|
||||
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
|
||||
}
|
||||
|
||||
// Get status filter
|
||||
status := toString(queryMap["status"])
|
||||
status := utils.ToString(queryMap["status"])
|
||||
|
||||
// Get context
|
||||
ctx := process.Context
|
||||
|
|
@ -947,7 +948,7 @@ func teamInvitationGetPublic(ctx context.Context, invitationID, locale string) (
|
|||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
|
|
@ -970,7 +971,7 @@ func teamInvitationGetPublic(ctx context.Context, invitationID, locale string) (
|
|||
}
|
||||
|
||||
// Get team information
|
||||
teamID := toString(invitationData["team_id"])
|
||||
teamID := utils.ToString(invitationData["team_id"])
|
||||
team, err := provider.GetTeam(ctx, teamID)
|
||||
if err != nil {
|
||||
log.Warn("Failed to get team information: %v", err)
|
||||
|
|
@ -978,25 +979,25 @@ func teamInvitationGetPublic(ctx context.Context, invitationID, locale string) (
|
|||
}
|
||||
|
||||
// Get inviter information
|
||||
inviterID := toString(invitationData["invited_by"])
|
||||
inviterID := utils.ToString(invitationData["invited_by"])
|
||||
var inviterInfo *InviterInfo
|
||||
if inviterID != "" {
|
||||
inviter, err := provider.GetUser(ctx, inviterID)
|
||||
if err == nil {
|
||||
inviterInfo = &InviterInfo{
|
||||
UserID: inviterID,
|
||||
Name: toString(inviter["name"]),
|
||||
Picture: toString(inviter["picture"]),
|
||||
Name: utils.ToString(inviter["name"]),
|
||||
Picture: utils.ToString(inviter["picture"]),
|
||||
}
|
||||
// Fallback to masked email if name is empty (for privacy protection)
|
||||
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
|
||||
roleID := toString(invitationData["role_id"])
|
||||
roleID := utils.ToString(invitationData["role_id"])
|
||||
roleLabel := ""
|
||||
teamConfig := GetTeamConfig(locale)
|
||||
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
|
||||
teamLogo := toString(team["logo"])
|
||||
teamLogo := utils.ToString(team["logo"])
|
||||
if teamLogo != "" {
|
||||
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)
|
||||
publicResponse := &PublicInvitationResponse{
|
||||
InvitationID: toString(invitationData["invitation_id"]),
|
||||
TeamName: toString(team["name"]),
|
||||
InvitationID: utils.ToString(invitationData["invitation_id"]),
|
||||
TeamName: utils.ToString(team["name"]),
|
||||
TeamLogo: teamLogo,
|
||||
TeamDescription: toString(team["description"]),
|
||||
TeamDescription: utils.ToString(team["description"]),
|
||||
RoleLabel: roleLabel,
|
||||
Status: toString(invitationData["status"]),
|
||||
InvitedAt: toTimeString(invitationData["invited_at"]),
|
||||
InvitationExpiresAt: toTimeString(invitationData["invitation_expires_at"]),
|
||||
Message: toString(invitationData["message"]),
|
||||
Status: utils.ToString(invitationData["status"]),
|
||||
InvitedAt: utils.ToTimeString(invitationData["invited_at"]),
|
||||
InvitationExpiresAt: utils.ToTimeString(invitationData["invitation_expires_at"]),
|
||||
Message: utils.ToString(invitationData["message"]),
|
||||
InviterInfo: inviterInfo,
|
||||
}
|
||||
|
||||
|
|
@ -1062,12 +1063,12 @@ func teamInvitationGet(ctx context.Context, userID, teamID, invitationID string)
|
|||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
|
|
@ -1108,7 +1109,7 @@ func teamInvitationCreate(ctx context.Context, userID, teamID string, invitation
|
|||
if err != nil {
|
||||
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
|
||||
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)
|
||||
inviter = maps.MapStrAny{"name": "Team Admin"}
|
||||
}
|
||||
inviterName := toString(inviter["name"])
|
||||
inviterName := utils.ToString(inviter["name"])
|
||||
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)
|
||||
|
|
@ -1126,10 +1127,10 @@ func teamInvitationCreate(ctx context.Context, userID, teamID string, invitation
|
|||
var inviteeEmail string
|
||||
|
||||
// Get email from invitation data first
|
||||
inviteeEmail = toString(invitationData["email"])
|
||||
inviteeEmail = utils.ToString(invitationData["email"])
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
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
|
||||
if inviteeEmail != "" {
|
||||
|
|
@ -1163,7 +1164,7 @@ func teamInvitationCreate(ctx context.Context, userID, teamID string, invitation
|
|||
shouldSendEmail = settings.SendEmail
|
||||
} else if settingsMap, ok := invitationData["settings"].(map[string]interface{}); ok {
|
||||
// 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
|
||||
|
|
@ -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)
|
||||
requestBaseURL := toString(invitationData["request_base_url"])
|
||||
requestBaseURL := utils.ToString(invitationData["request_base_url"])
|
||||
savedSettings := invitationData["settings"] // Save settings reference
|
||||
|
||||
// Set invitation-specific fields
|
||||
|
|
@ -1213,7 +1214,7 @@ func teamInvitationCreate(ctx context.Context, userID, teamID string, invitation
|
|||
}
|
||||
|
||||
// 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)
|
||||
if shouldSendEmail {
|
||||
|
|
@ -1270,17 +1271,17 @@ func teamInvitationResend(ctx context.Context, userID, teamID, invitationID, req
|
|||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// Get email directly from member record's email field
|
||||
inviteeEmail := toString(invitationData["email"])
|
||||
inviteeEmail := utils.ToString(invitationData["email"])
|
||||
if inviteeEmail == "" {
|
||||
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 {
|
||||
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
|
||||
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)
|
||||
inviter = maps.MapStrAny{"name": "Team Admin"}
|
||||
}
|
||||
inviterName := toString(inviter["name"])
|
||||
inviterName := utils.ToString(inviter["name"])
|
||||
if inviterName == "" {
|
||||
inviterName = toString(inviter["email"])
|
||||
inviterName = utils.ToString(inviter["email"])
|
||||
}
|
||||
|
||||
// Generate new invitation token
|
||||
|
|
@ -1402,12 +1403,12 @@ func teamInvitationDelete(ctx context.Context, userID, teamID, invitationID stri
|
|||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
|
|
@ -1440,7 +1441,7 @@ func getTeamInvitationExpiry(invitationData maps.MapStrAny) (time.Duration, erro
|
|||
defaultExpiry := 7 * 24 * time.Hour
|
||||
|
||||
// Check if expiry is provided in request
|
||||
expiry := toString(invitationData["expiry"])
|
||||
expiry := utils.ToString(invitationData["expiry"])
|
||||
if expiry != "" {
|
||||
normalizedDuration, err := normalizeDuration(expiry)
|
||||
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 {
|
||||
// Fallback for map format (for backward compatibility)
|
||||
if loc := toString(settingsMap["locale"]); loc != "" {
|
||||
if loc := utils.ToString(settingsMap["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 {
|
||||
// Fallback for map format (for backward compatibility)
|
||||
if loc := toString(settingsMap["locale"]); loc != "" {
|
||||
if loc := utils.ToString(settingsMap["locale"]); loc != "" {
|
||||
locale = loc
|
||||
}
|
||||
}
|
||||
|
|
@ -1530,19 +1531,19 @@ func sendTeamInvitationEmail(ctx context.Context, email, inviterName, teamName,
|
|||
}
|
||||
|
||||
// Get custom message from invitation data
|
||||
customMessage := toString(invitationData["message"])
|
||||
customMessage := utils.ToString(invitationData["message"])
|
||||
|
||||
// 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
|
||||
invitationLink := buildTeamInvitationLink(invitationID, token, teamConfig, requestBaseURL)
|
||||
|
||||
// Get time format based on locale
|
||||
timeFormat := getTimeFormat(locale)
|
||||
timeFormat := utils.GetTimeFormat(locale)
|
||||
|
||||
// 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
|
||||
templateData := messengertypes.TemplateData{
|
||||
|
|
@ -1553,7 +1554,7 @@ func sendTeamInvitationEmail(ctx context.Context, email, inviterName, teamName,
|
|||
"invitation_link": invitationLink, // Full invitation link
|
||||
"token": token, // Keep token for backward compatibility
|
||||
"message": customMessage,
|
||||
"role_id": toString(invitationData["role_id"]),
|
||||
"role_id": utils.ToString(invitationData["role_id"]),
|
||||
"expires_at": expiresAtFormatted,
|
||||
}
|
||||
|
||||
|
|
@ -1574,20 +1575,20 @@ func convertToTeamInvitationResponse(data maps.MapStrAny, requestBaseURL string)
|
|||
// mapToTeamInvitationResponse converts a map to InvitationResponse
|
||||
func mapToTeamInvitationResponse(data maps.MapStr, requestBaseURL string) InvitationResponse {
|
||||
invitation := InvitationResponse{
|
||||
ID: toInt64(data["id"]),
|
||||
InvitationID: toString(data["invitation_id"]),
|
||||
TeamID: toString(data["team_id"]),
|
||||
UserID: toString(data["user_id"]),
|
||||
MemberType: toString(data["member_type"]),
|
||||
RoleID: toString(data["role_id"]),
|
||||
Status: toString(data["status"]),
|
||||
InvitedBy: toString(data["invited_by"]),
|
||||
InvitedAt: toTimeString(data["invited_at"]),
|
||||
InvitationToken: toString(data["invitation_token"]),
|
||||
InvitationExpiresAt: toTimeString(data["invitation_expires_at"]),
|
||||
Message: toString(data["message"]),
|
||||
CreatedAt: toTimeString(data["created_at"]),
|
||||
UpdatedAt: toTimeString(data["updated_at"]),
|
||||
ID: utils.ToInt64(data["id"]),
|
||||
InvitationID: utils.ToString(data["invitation_id"]),
|
||||
TeamID: utils.ToString(data["team_id"]),
|
||||
UserID: utils.ToString(data["user_id"]),
|
||||
MemberType: utils.ToString(data["member_type"]),
|
||||
RoleID: utils.ToString(data["role_id"]),
|
||||
Status: utils.ToString(data["status"]),
|
||||
InvitedBy: utils.ToString(data["invited_by"]),
|
||||
InvitedAt: utils.ToTimeString(data["invited_at"]),
|
||||
InvitationToken: utils.ToString(data["invitation_token"]),
|
||||
InvitationExpiresAt: utils.ToTimeString(data["invitation_expires_at"]),
|
||||
Message: utils.ToString(data["message"]),
|
||||
CreatedAt: utils.ToTimeString(data["created_at"]),
|
||||
UpdatedAt: utils.ToTimeString(data["updated_at"]),
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Convert map to InvitationSettings
|
||||
invSettings := &InvitationSettings{
|
||||
SendEmail: toBool(settingsMap["send_email"]),
|
||||
Locale: toString(settingsMap["locale"]),
|
||||
SendEmail: utils.ToBool(settingsMap["send_email"]),
|
||||
Locale: utils.ToString(settingsMap["locale"]),
|
||||
}
|
||||
invitation.Settings = invSettings
|
||||
if invSettings.Locale != "" {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/gou/process"
|
||||
|
|
@ -31,274 +28,6 @@ func GetUserIDFromSession(process *process.Process) string {
|
|||
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
|
||||
|
||||
// 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",
|
||||
"label": "System Collection",
|
||||
"comment": "Whether this is a system collection",
|
||||
"label": "Preset Collection",
|
||||
"comment": "Whether this is a preset collection",
|
||||
"default": false,
|
||||
"nullable": false
|
||||
},
|
||||
{
|
||||
"name": "readonly",
|
||||
"name": "public",
|
||||
"type": "boolean",
|
||||
"label": "Readonly Collection",
|
||||
"comment": "Whether this collection is read-only",
|
||||
"index": true,
|
||||
"label": "Public Collection",
|
||||
"comment": "Whether this collection is shared across all teams in the platform",
|
||||
"default": 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",
|
||||
"type": "integer",
|
||||
|
|
@ -193,8 +205,5 @@
|
|||
"default": 10
|
||||
}
|
||||
],
|
||||
"option": {
|
||||
"permission": true,
|
||||
"timestamps": true
|
||||
}
|
||||
"option": { "soft_deletes": true, "permission": true, "timestamps": true }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,21 +133,34 @@
|
|||
"default": "en"
|
||||
},
|
||||
{
|
||||
"name": "system",
|
||||
"name": "preset",
|
||||
"type": "boolean",
|
||||
"label": "System Document",
|
||||
"comment": "Whether this is a system document",
|
||||
"label": "Preset Document",
|
||||
"comment": "Whether this is a preset document",
|
||||
"default": false,
|
||||
"nullable": false
|
||||
},
|
||||
{
|
||||
"name": "readonly",
|
||||
"name": "public",
|
||||
"type": "boolean",
|
||||
"label": "Readonly Document",
|
||||
"comment": "Whether this document is read-only",
|
||||
"label": "Public Document",
|
||||
"comment": "Whether this document is shared across all teams in the platform",
|
||||
"default": 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",
|
||||
"type": "integer",
|
||||
|
|
@ -352,8 +365,5 @@
|
|||
"nullable": true
|
||||
}
|
||||
],
|
||||
"option": {
|
||||
"permission": true,
|
||||
"timestamps": true
|
||||
}
|
||||
"option": { "soft_deletes": true, "permission": true, "timestamps": true }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue