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:
Max 2025-10-31 11:05:46 +08:00
parent 0ee38a1cb3
commit d17832c016
12 changed files with 622 additions and 577 deletions

File diff suppressed because one or more lines are too long

View file

@ -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{

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"]),
} }
} }

View file

@ -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
} }

View file

@ -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 != "" {

View file

@ -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
View 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)
}

View file

@ -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
}
} }

View file

@ -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
}
} }