feat(profile): options to select why kind of share links to show in user's profile (#2453)

pull/2481/merge
Aaron Liu 2025-08-12 09:52:47 +08:00
parent bb3db2e326
commit b0057fe92f
7 changed files with 101 additions and 65 deletions

2
assets

@ -1 +1 @@
Subproject commit 8b2c8a7bdbde43a1f95ddaa4555e4304215c1e7c Subproject commit eb2cfac37d73e5bd3000eb66a3a0062509efe122

View File

@ -7,17 +7,20 @@ import (
// UserSetting 用户其他配置 // UserSetting 用户其他配置
type ( type (
UserSetting struct { UserSetting struct {
ProfileOff bool `json:"profile_off,omitempty"` ProfileOff bool `json:"profile_off,omitempty"`
PreferredTheme string `json:"preferred_theme,omitempty"` PreferredTheme string `json:"preferred_theme,omitempty"`
VersionRetention bool `json:"version_retention,omitempty"` VersionRetention bool `json:"version_retention,omitempty"`
VersionRetentionExt []string `json:"version_retention_ext,omitempty"` VersionRetentionExt []string `json:"version_retention_ext,omitempty"`
VersionRetentionMax int `json:"version_retention_max,omitempty"` VersionRetentionMax int `json:"version_retention_max,omitempty"`
Pined []PinedFile `json:"pined,omitempty"` Pined []PinedFile `json:"pined,omitempty"`
Language string `json:"email_language,omitempty"` Language string `json:"email_language,omitempty"`
DisableViewSync bool `json:"disable_view_sync,omitempty"` DisableViewSync bool `json:"disable_view_sync,omitempty"`
FsViewMap map[string]ExplorerView `json:"fs_view_map,omitempty"` FsViewMap map[string]ExplorerView `json:"fs_view_map,omitempty"`
ShareLinksInProfile ShareLinksInProfileLevel `json:"share_links_in_profile,omitempty"`
} }
ShareLinksInProfileLevel string
PinedFile struct { PinedFile struct {
Uri string `json:"uri"` Uri string `json:"uri"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
@ -334,3 +337,9 @@ const (
CustomPropsTypeLink = "link" CustomPropsTypeLink = "link"
CustomPropsTypeRating = "rating" CustomPropsTypeRating = "rating"
) )
const (
ProfilePublicShareOnly = ShareLinksInProfileLevel("")
ProfileAllShare = ShareLinksInProfileLevel("all_share")
ProfileHideShare = ShareLinksInProfileLevel("hide_share")
)

View File

@ -271,19 +271,20 @@ type Entity struct {
} }
type Share struct { type Share struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
RemainDownloads *int `json:"remain_downloads,omitempty"` RemainDownloads *int `json:"remain_downloads,omitempty"`
Visited int `json:"visited"` Visited int `json:"visited"`
Downloaded int `json:"downloaded,omitempty"` Downloaded int `json:"downloaded,omitempty"`
Expires *time.Time `json:"expires,omitempty"` Expires *time.Time `json:"expires,omitempty"`
Unlocked bool `json:"unlocked"` Unlocked bool `json:"unlocked"`
SourceType *types.FileType `json:"source_type,omitempty"` PasswordProtected bool `json:"password_protected,omitempty"`
Owner user.User `json:"owner"` SourceType *types.FileType `json:"source_type,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"` Owner user.User `json:"owner"`
Expired bool `json:"expired"` CreatedAt time.Time `json:"created_at,omitempty"`
Url string `json:"url"` Expired bool `json:"expired"`
ShowReadMe bool `json:"show_readme,omitempty"` Url string `json:"url"`
ShowReadMe bool `json:"show_readme,omitempty"`
// Only viewable by owner // Only viewable by owner
IsPrivate bool `json:"is_private,omitempty"` IsPrivate bool `json:"is_private,omitempty"`
@ -301,15 +302,16 @@ func BuildShare(s *ent.Share, base *url.URL, hasher hashid.Encoder, requester *e
redactLevel = user.RedactLevelUser redactLevel = user.RedactLevelUser
} }
res := Share{ res := Share{
Name: name, Name: name,
ID: hashid.EncodeShareID(hasher, s.ID), ID: hashid.EncodeShareID(hasher, s.ID),
Unlocked: unlocked, Unlocked: unlocked,
Owner: user.BuildUserRedacted(owner, redactLevel, hasher), Owner: user.BuildUserRedacted(owner, redactLevel, hasher),
Expired: inventory.IsShareExpired(s) != nil || expired, Expired: inventory.IsShareExpired(s) != nil || expired,
Url: BuildShareLink(s, hasher, base), Url: BuildShareLink(s, hasher, base, unlocked),
CreatedAt: s.CreatedAt, CreatedAt: s.CreatedAt,
Visited: s.Views, Visited: s.Views,
SourceType: util.ToPtr(t), SourceType: util.ToPtr(t),
PasswordProtected: s.Password != "",
} }
if unlocked { if unlocked {
@ -436,9 +438,12 @@ func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.E
} }
} }
func BuildShareLink(s *ent.Share, hasher hashid.Encoder, base *url.URL) string { func BuildShareLink(s *ent.Share, hasher hashid.Encoder, base *url.URL, unlocked bool) string {
shareId := hashid.EncodeShareID(hasher, s.ID) shareId := hashid.EncodeShareID(hasher, s.ID)
return routes.MasterShareUrl(base, shareId, s.Password).String() if unlocked {
return routes.MasterShareUrl(base, shareId, s.Password).String()
}
return routes.MasterShareUrl(base, shareId, "").String()
} }
func BuildStoragePolicy(sp *ent.StoragePolicy, hasher hashid.Encoder) *StoragePolicy { func BuildStoragePolicy(sp *ent.StoragePolicy, hasher hashid.Encoder) *StoragePolicy {

View File

@ -66,7 +66,7 @@ func (service *ShareCreateService) Upsert(c *gin.Context, existed int) (string,
} }
base := dep.SettingProvider().SiteURL(c) base := dep.SettingProvider().SiteURL(c)
return explorer.BuildShareLink(share, dep.HashIDEncoder(), base), nil return explorer.BuildShareLink(share, dep.HashIDEncoder(), base, true), nil
} }
func DeleteShare(c *gin.Context, shareId int) error { func DeleteShare(c *gin.Context, shareId int) error {

View File

@ -137,6 +137,16 @@ func (s *ListShareService) ListInUserProfile(c *gin.Context, uid int) (*ListShar
hasher := dep.HashIDEncoder() hasher := dep.HashIDEncoder()
shareClient := dep.ShareClient() shareClient := dep.ShareClient()
targetUser, err := dep.UserClient().GetActiveByID(c, uid)
if err != nil {
return nil, serializer.NewError(serializer.CodeDBError, "Failed to get user", err)
}
if targetUser.Settings != nil && targetUser.Settings.ShareLinksInProfile == types.ProfileHideShare {
return nil, serializer.NewError(serializer.CodeParamErr, "User has disabled share links in profile", nil)
}
publicOnly := targetUser.Settings == nil || targetUser.Settings.ShareLinksInProfile == types.ProfilePublicShareOnly
args := &inventory.ListShareArgs{ args := &inventory.ListShareArgs{
PaginationArgs: &inventory.PaginationArgs{ PaginationArgs: &inventory.PaginationArgs{
UseCursorPagination: true, UseCursorPagination: true,
@ -146,7 +156,7 @@ func (s *ListShareService) ListInUserProfile(c *gin.Context, uid int) (*ListShar
OrderBy: s.OrderBy, OrderBy: s.OrderBy,
}, },
UserID: uid, UserID: uid,
PublicOnly: true, PublicOnly: publicOnly,
} }
ctx := context.WithValue(c, inventory.LoadShareUser{}, true) ctx := context.WithValue(c, inventory.LoadShareUser{}, true)

View File

@ -29,6 +29,7 @@ type UserSettings struct {
TwoFAEnabled bool `json:"two_fa_enabled"` TwoFAEnabled bool `json:"two_fa_enabled"`
Passkeys []Passkey `json:"passkeys,omitempty"` Passkeys []Passkey `json:"passkeys,omitempty"`
DisableViewSync bool `json:"disable_view_sync"` DisableViewSync bool `json:"disable_view_sync"`
ShareLinksInProfile string `json:"share_links_in_profile"`
} }
func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Parser) *UserSettings { func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Parser) *UserSettings {
@ -41,7 +42,8 @@ func BuildUserSettings(u *ent.User, passkeys []*ent.Passkey, parser *uaparser.Pa
Passkeys: lo.Map(passkeys, func(item *ent.Passkey, index int) Passkey { Passkeys: lo.Map(passkeys, func(item *ent.Passkey, index int) Passkey {
return BuildPasskey(item) return BuildPasskey(item)
}), }),
DisableViewSync: u.Settings.DisableViewSync, DisableViewSync: u.Settings.DisableViewSync,
ShareLinksInProfile: string(u.Settings.ShareLinksInProfile),
} }
} }
@ -97,18 +99,19 @@ type BuiltinLoginResponse struct {
// User 用户序列化器 // User 用户序列化器
type User struct { type User struct {
ID string `json:"id"` ID string `json:"id"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
Status user.Status `json:"status,omitempty"` Status user.Status `json:"status,omitempty"`
Avatar string `json:"avatar,omitempty"` Avatar string `json:"avatar,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
PreferredTheme string `json:"preferred_theme,omitempty"` PreferredTheme string `json:"preferred_theme,omitempty"`
Anonymous bool `json:"anonymous,omitempty"` Anonymous bool `json:"anonymous,omitempty"`
Group *Group `json:"group,omitempty"` Group *Group `json:"group,omitempty"`
Pined []types.PinedFile `json:"pined,omitempty"` Pined []types.PinedFile `json:"pined,omitempty"`
Language string `json:"language,omitempty"` Language string `json:"language,omitempty"`
DisableViewSync bool `json:"disable_view_sync,omitempty"` DisableViewSync bool `json:"disable_view_sync,omitempty"`
ShareLinksInProfile types.ShareLinksInProfileLevel `json:"share_links_in_profile,omitempty"`
} }
type Group struct { type Group struct {
@ -153,18 +156,19 @@ func BuildWebAuthnList(credentials []webauthn.Credential) []WebAuthnCredentials
// BuildUser 序列化用户 // BuildUser 序列化用户
func BuildUser(user *ent.User, idEncoder hashid.Encoder) User { func BuildUser(user *ent.User, idEncoder hashid.Encoder) User {
return User{ return User{
ID: hashid.EncodeUserID(idEncoder, user.ID), ID: hashid.EncodeUserID(idEncoder, user.ID),
Email: user.Email, Email: user.Email,
Nickname: user.Nick, Nickname: user.Nick,
Status: user.Status, Status: user.Status,
Avatar: user.Avatar, Avatar: user.Avatar,
CreatedAt: user.CreatedAt, CreatedAt: user.CreatedAt,
PreferredTheme: user.Settings.PreferredTheme, PreferredTheme: user.Settings.PreferredTheme,
Anonymous: user.ID == 0, Anonymous: user.ID == 0,
Group: BuildGroup(user.Edges.Group, idEncoder), Group: BuildGroup(user.Edges.Group, idEncoder),
Pined: user.Settings.Pined, Pined: user.Settings.Pined,
Language: user.Settings.Language, Language: user.Settings.Language,
DisableViewSync: user.Settings.DisableViewSync, DisableViewSync: user.Settings.DisableViewSync,
ShareLinksInProfile: user.Settings.ShareLinksInProfile,
} }
} }
@ -193,10 +197,11 @@ func BuildUserRedacted(u *ent.User, level int, idEncoder hashid.Encoder) User {
userRaw := BuildUser(u, idEncoder) userRaw := BuildUser(u, idEncoder)
user := User{ user := User{
ID: userRaw.ID, ID: userRaw.ID,
Nickname: userRaw.Nickname, Nickname: userRaw.Nickname,
Avatar: userRaw.Avatar, Avatar: userRaw.Avatar,
CreatedAt: userRaw.CreatedAt, CreatedAt: userRaw.CreatedAt,
ShareLinksInProfile: userRaw.ShareLinksInProfile,
} }
if userRaw.Group != nil { if userRaw.Group != nil {

View File

@ -14,6 +14,7 @@ import (
"github.com/cloudreve/Cloudreve/v4/application/dependency" "github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/inventory" "github.com/cloudreve/Cloudreve/v4/inventory"
"github.com/cloudreve/Cloudreve/v4/inventory/types"
"github.com/cloudreve/Cloudreve/v4/pkg/hashid" "github.com/cloudreve/Cloudreve/v4/pkg/hashid"
"github.com/cloudreve/Cloudreve/v4/pkg/request" "github.com/cloudreve/Cloudreve/v4/pkg/request"
"github.com/cloudreve/Cloudreve/v4/pkg/serializer" "github.com/cloudreve/Cloudreve/v4/pkg/serializer"
@ -221,6 +222,7 @@ type (
TwoFAEnabled *bool `json:"two_fa_enabled" binding:"omitempty"` TwoFAEnabled *bool `json:"two_fa_enabled" binding:"omitempty"`
TwoFACode *string `json:"two_fa_code" binding:"omitempty"` TwoFACode *string `json:"two_fa_code" binding:"omitempty"`
DisableViewSync *bool `json:"disable_view_sync" binding:"omitempty"` DisableViewSync *bool `json:"disable_view_sync" binding:"omitempty"`
ShareLinksInProfile *string `json:"share_links_in_profile" binding:"omitempty"`
} }
PatchUserSettingParamsCtx struct{} PatchUserSettingParamsCtx struct{}
) )
@ -267,6 +269,11 @@ func (s *PatchUserSetting) Patch(c *gin.Context) error {
saveSetting = true saveSetting = true
} }
if s.ShareLinksInProfile != nil {
u.Settings.ShareLinksInProfile = types.ShareLinksInProfileLevel(*s.ShareLinksInProfile)
saveSetting = true
}
if s.CurrentPassword != nil && s.NewPassword != nil { if s.CurrentPassword != nil && s.NewPassword != nil {
if err := inventory.CheckPassword(u, *s.CurrentPassword); err != nil { if err := inventory.CheckPassword(u, *s.CurrentPassword); err != nil {
return serializer.NewError(serializer.CodeIncorrectPassword, "Incorrect password", err) return serializer.NewError(serializer.CodeIncorrectPassword, "Incorrect password", err)