From baed01f24753afb600a2984dcb9bcda0bb8502b6 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 22 Nov 2021 23:21:55 +0800 Subject: [PATCH] Remove unnecessary attributes of User struct (#17745) * Remove unnecessary functions of User struct * Move more database methods out of user struct * Move more database methods out of user struct * Fix template failure * Fix bug * Remove finished FIXME * remove unnecessary code --- cmd/admin.go | 2 +- models/access.go | 60 --------- models/access_test.go | 33 ----- models/org.go | 20 +-- models/repo.go | 31 +++-- models/star.go | 47 +------ models/star_test.go | 50 +------- models/user.go | 118 +++--------------- models/user_avatar.go | 62 +-------- models/user_email.go | 21 ++++ models/user_test.go | 14 ++- modules/context/org.go | 4 + modules/context/repo.go | 7 +- routers/api/v1/org/member.go | 7 +- routers/api/v1/org/org.go | 3 +- routers/api/v1/repo/pull.go | 6 +- routers/api/v1/repo/star.go | 3 +- routers/api/v1/user/follower.go | 8 +- routers/api/v1/user/user.go | 2 +- routers/web/admin/users.go | 12 +- routers/web/org/home.go | 1 - routers/web/org/setting.go | 7 +- routers/web/repo/branch.go | 3 +- routers/web/repo/compare.go | 10 +- routers/web/repo/issue.go | 2 +- routers/web/repo/middlewares.go | 3 +- routers/web/repo/pull.go | 18 +-- routers/web/repo/repo.go | 2 +- routers/web/repo/view.go | 4 +- routers/web/user/auth.go | 17 +-- routers/web/user/home.go | 6 +- routers/web/user/profile.go | 16 ++- routers/web/user/setting/account.go | 5 +- routers/web/user/setting/profile.go | 34 ++--- services/auth/auth.go | 3 +- services/auth/source/db/authenticate.go | 3 +- .../auth/source/ldap/source_authenticate.go | 6 +- services/auth/source/ldap/source_sync.go | 9 +- services/user/user.go | 65 ++++++++++ templates/org/home.tmpl | 2 +- templates/user/dashboard/repolist.tmpl | 2 +- templates/user/profile.tmpl | 2 +- 42 files changed, 279 insertions(+), 451 deletions(-) diff --git a/cmd/admin.go b/cmd/admin.go index 27089a17c496..b4fc7f570671 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -366,7 +366,7 @@ func runChangePassword(c *cli.Context) error { return err } - if err = models.UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil { + if err = models.UpdateUserCols(db.DefaultContext, user, "passwd", "passwd_hash_algo", "salt"); err != nil { return err } diff --git a/models/access.go b/models/access.go index 500bcc1e1909..7af88e1b4a1e 100644 --- a/models/access.go +++ b/models/access.go @@ -105,66 +105,6 @@ func accessLevel(e db.Engine, user *User, repo *Repository) (AccessMode, error) return a.Mode, nil } -type repoAccess struct { - Access `xorm:"extends"` - Repository `xorm:"extends"` -} - -func (repoAccess) TableName() string { - return "access" -} - -// GetRepositoryAccesses finds all repositories with their access mode where a user has access but does not own. -func (user *User) GetRepositoryAccesses() (map[*Repository]AccessMode, error) { - rows, err := db.GetEngine(db.DefaultContext). - Join("INNER", "repository", "repository.id = access.repo_id"). - Where("access.user_id = ?", user.ID). - And("repository.owner_id <> ?", user.ID). - Rows(new(repoAccess)) - if err != nil { - return nil, err - } - defer rows.Close() - - repos := make(map[*Repository]AccessMode, 10) - ownerCache := make(map[int64]*User, 10) - for rows.Next() { - var repo repoAccess - err = rows.Scan(&repo) - if err != nil { - return nil, err - } - - var ok bool - if repo.Owner, ok = ownerCache[repo.OwnerID]; !ok { - if err = repo.GetOwner(); err != nil { - return nil, err - } - ownerCache[repo.OwnerID] = repo.Owner - } - - repos[&repo.Repository] = repo.Access.Mode - } - return repos, nil -} - -// GetAccessibleRepositories finds repositories which the user has access but does not own. -// If limit is smaller than 1 means returns all found results. -func (user *User) GetAccessibleRepositories(limit int) (repos []*Repository, _ error) { - sess := db.GetEngine(db.DefaultContext). - Where("owner_id !=? ", user.ID). - Desc("updated_unix") - if limit > 0 { - sess.Limit(limit) - repos = make([]*Repository, 0, limit) - } else { - repos = make([]*Repository, 0, 10) - } - return repos, sess. - Join("INNER", "access", "access.user_id = ? AND access.repo_id = repository.id", user.ID). - Find(&repos) -} - func maxAccessMode(modes ...AccessMode) AccessMode { max := AccessModeNone for _, mode := range modes { diff --git a/models/access_test.go b/models/access_test.go index 942ca4af4286..81e36ed2e024 100644 --- a/models/access_test.go +++ b/models/access_test.go @@ -90,39 +90,6 @@ func TestHasAccess(t *testing.T) { assert.NoError(t, err) } -func TestUser_GetRepositoryAccesses(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - user1 := unittest.AssertExistsAndLoadBean(t, &User{ID: 1}).(*User) - accesses, err := user1.GetRepositoryAccesses() - assert.NoError(t, err) - assert.Len(t, accesses, 0) - - user29 := unittest.AssertExistsAndLoadBean(t, &User{ID: 29}).(*User) - accesses, err = user29.GetRepositoryAccesses() - assert.NoError(t, err) - assert.Len(t, accesses, 2) -} - -func TestUser_GetAccessibleRepositories(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - user1 := unittest.AssertExistsAndLoadBean(t, &User{ID: 1}).(*User) - repos, err := user1.GetAccessibleRepositories(0) - assert.NoError(t, err) - assert.Len(t, repos, 0) - - user2 := unittest.AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) - repos, err = user2.GetAccessibleRepositories(0) - assert.NoError(t, err) - assert.Len(t, repos, 4) - - user29 := unittest.AssertExistsAndLoadBean(t, &User{ID: 29}).(*User) - repos, err = user29.GetAccessibleRepositories(0) - assert.NoError(t, err) - assert.Len(t, repos, 2) -} - func TestRepository_RecalculateAccesses(t *testing.T) { // test with organization repo assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/org.go b/models/org.go index b3e67fb52134..ea73d03d422e 100644 --- a/models/org.go +++ b/models/org.go @@ -125,6 +125,12 @@ func (org *Organization) HomeLink() string { return org.AsUser().HomeLink() } +// CanCreateRepo returns if user login can create a repository +// NOTE: functions calling this assume a failure due to repository count limit; if new checks are added, those functions should be revised +func (org *Organization) CanCreateRepo() bool { + return org.AsUser().CanCreateRepo() +} + // FindOrgMembersOpts represensts find org members conditions type FindOrgMembersOpts struct { db.ListOptions @@ -240,7 +246,7 @@ func CreateOrganization(org *Organization, owner *User) (err error) { if err = db.Insert(ctx, org); err != nil { return fmt.Errorf("insert organization: %v", err) } - if err = org.AsUser().generateRandomAvatar(db.GetEngine(ctx)); err != nil { + if err = generateRandomAvatar(db.GetEngine(ctx), org.AsUser()); err != nil { return fmt.Errorf("generate random avatar: %v", err) } @@ -546,8 +552,8 @@ func CountOrgs(opts FindOrgOptions) (int64, error) { Count(new(User)) } -func getOwnedOrgsByUserID(sess db.Engine, userID int64) ([]*User, error) { - orgs := make([]*User, 0, 10) +func getOwnedOrgsByUserID(sess db.Engine, userID int64) ([]*Organization, error) { + orgs := make([]*Organization, 0, 10) return orgs, sess. Join("INNER", "`team_user`", "`team_user`.org_id=`user`.id"). Join("INNER", "`team`", "`team`.id=`team_user`.team_id"). @@ -593,20 +599,20 @@ func HasOrgsVisible(orgs []*Organization, user *User) bool { } // GetOwnedOrgsByUserID returns a list of organizations are owned by given user ID. -func GetOwnedOrgsByUserID(userID int64) ([]*User, error) { +func GetOwnedOrgsByUserID(userID int64) ([]*Organization, error) { return getOwnedOrgsByUserID(db.GetEngine(db.DefaultContext), userID) } // GetOwnedOrgsByUserIDDesc returns a list of organizations are owned by // given user ID, ordered descending by the given condition. -func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) { +func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*Organization, error) { return getOwnedOrgsByUserID(db.GetEngine(db.DefaultContext).Desc(desc), userID) } // GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID // are allowed to create repos. -func GetOrgsCanCreateRepoByUserID(userID int64) ([]*User, error) { - orgs := make([]*User, 0, 10) +func GetOrgsCanCreateRepoByUserID(userID int64) ([]*Organization, error) { + orgs := make([]*Organization, 0, 10) return orgs, db.GetEngine(db.DefaultContext).Where(builder.In("id", builder.Select("`user`.id").From("`user`"). Join("INNER", "`team_user`", "`team_user`.org_id = `user`.id"). diff --git a/models/repo.go b/models/repo.go index 1347de304582..7944149a7687 100644 --- a/models/repo.go +++ b/models/repo.go @@ -754,19 +754,20 @@ func (repo *Repository) UpdateSize(ctx context.Context) error { return repo.updateSize(db.GetEngine(ctx)) } -// CanUserFork returns true if specified user can fork repository. -func (repo *Repository) CanUserFork(user *User) (bool, error) { +// CanUserForkRepo returns true if specified user can fork repository. +func CanUserForkRepo(user *User, repo *Repository) (bool, error) { if user == nil { return false, nil } - if repo.OwnerID != user.ID && !user.HasForkedRepo(repo.ID) { + if repo.OwnerID != user.ID && !HasForkedRepo(user.ID, repo.ID) { return true, nil } - if err := user.GetOwnedOrganizations(); err != nil { + ownedOrgs, err := GetOwnedOrgsByUserID(user.ID) + if err != nil { return false, err } - for _, org := range user.OwnedOrgs { - if repo.OwnerID != org.ID && !org.HasForkedRepo(repo.ID) { + for _, org := range ownedOrgs { + if repo.OwnerID != org.ID && !HasForkedRepo(org.ID, repo.ID) { return true, nil } } @@ -2036,13 +2037,25 @@ func (repo *Repository) SetArchiveRepoState(isArchived bool) (err error) { // \___ / \____/|__| |__|_ \ // \/ \/ -// HasForkedRepo checks if given user has already forked a repository with given ID. -func HasForkedRepo(ownerID, repoID int64) (*Repository, bool) { +// GetForkedRepo checks if given user has already forked a repository with given ID. +func GetForkedRepo(ownerID, repoID int64) *Repository { repo := new(Repository) has, _ := db.GetEngine(db.DefaultContext). Where("owner_id=? AND fork_id=?", ownerID, repoID). Get(repo) - return repo, has + if has { + return repo + } + return nil +} + +// HasForkedRepo checks if given user has already forked a repository with given ID. +func HasForkedRepo(ownerID, repoID int64) bool { + has, _ := db.GetEngine(db.DefaultContext). + Table("repository"). + Where("owner_id=? AND fork_id=?", ownerID, repoID). + Exist() + return has } // CopyLFS copies LFS data from one repo to another diff --git a/models/star.go b/models/star.go index baf724d7e08f..c0b15be21a0a 100644 --- a/models/star.go +++ b/models/star.go @@ -74,7 +74,7 @@ func isStaring(e db.Engine, userID, repoID int64) bool { } // GetStargazers returns the users that starred the repo. -func (repo *Repository) GetStargazers(opts db.ListOptions) ([]*User, error) { +func GetStargazers(repo *Repository, opts db.ListOptions) ([]*User, error) { sess := db.GetEngine(db.DefaultContext).Where("star.repo_id = ?", repo.ID). Join("LEFT", "star", "`user`.id = star.uid") if opts.Page > 0 { @@ -87,48 +87,3 @@ func (repo *Repository) GetStargazers(opts db.ListOptions) ([]*User, error) { users := make([]*User, 0, 8) return users, sess.Find(&users) } - -// GetStarredRepos returns the repos the user starred. -func (u *User) GetStarredRepos(private bool, page, pageSize int, orderBy string) (repos RepositoryList, err error) { - if len(orderBy) == 0 { - orderBy = "updated_unix DESC" - } - sess := db.GetEngine(db.DefaultContext). - Join("INNER", "star", "star.repo_id = repository.id"). - Where("star.uid = ?", u.ID). - OrderBy(orderBy) - - if !private { - sess = sess.And("is_private = ?", false) - } - - if page <= 0 { - page = 1 - } - sess.Limit(pageSize, (page-1)*pageSize) - - repos = make([]*Repository, 0, pageSize) - - if err = sess.Find(&repos); err != nil { - return - } - - if err = repos.loadAttributes(db.GetEngine(db.DefaultContext)); err != nil { - return - } - - return -} - -// GetStarredRepoCount returns the numbers of repo the user starred. -func (u *User) GetStarredRepoCount(private bool) (int64, error) { - sess := db.GetEngine(db.DefaultContext). - Join("INNER", "star", "star.repo_id = repository.id"). - Where("star.uid = ?", u.ID) - - if !private { - sess = sess.And("is_private = ?", false) - } - - return sess.Count(&Repository{}) -} diff --git a/models/star_test.go b/models/star_test.go index f9a7ddb0f2af..eef1a0c5fb9f 100644 --- a/models/star_test.go +++ b/models/star_test.go @@ -36,7 +36,7 @@ func TestRepository_GetStargazers(t *testing.T) { // repo with stargazers assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 4}).(*Repository) - gazers, err := repo.GetStargazers(db.ListOptions{Page: 0}) + gazers, err := GetStargazers(repo, db.ListOptions{Page: 0}) assert.NoError(t, err) if assert.Len(t, gazers, 1) { assert.Equal(t, int64(2), gazers[0].ID) @@ -47,53 +47,7 @@ func TestRepository_GetStargazers2(t *testing.T) { // repo with stargazers assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository) - gazers, err := repo.GetStargazers(db.ListOptions{Page: 0}) + gazers, err := GetStargazers(repo, db.ListOptions{Page: 0}) assert.NoError(t, err) assert.Len(t, gazers, 0) } - -func TestUser_GetStarredRepos(t *testing.T) { - // user who has starred repos - assert.NoError(t, unittest.PrepareTestDatabase()) - - user := unittest.AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) - starred, err := user.GetStarredRepos(false, 1, 10, "") - assert.NoError(t, err) - if assert.Len(t, starred, 1) { - assert.Equal(t, int64(4), starred[0].ID) - } - - starred, err = user.GetStarredRepos(true, 1, 10, "") - assert.NoError(t, err) - if assert.Len(t, starred, 2) { - assert.Equal(t, int64(2), starred[0].ID) - assert.Equal(t, int64(4), starred[1].ID) - } -} - -func TestUser_GetStarredRepos2(t *testing.T) { - // user who has no starred repos - assert.NoError(t, unittest.PrepareTestDatabase()) - - user := unittest.AssertExistsAndLoadBean(t, &User{ID: 1}).(*User) - starred, err := user.GetStarredRepos(false, 1, 10, "") - assert.NoError(t, err) - assert.Len(t, starred, 0) - - starred, err = user.GetStarredRepos(true, 1, 10, "") - assert.NoError(t, err) - assert.Len(t, starred, 0) -} - -func TestUserGetStarredRepoCount(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - user := unittest.AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) - counts, err := user.GetStarredRepoCount(false) - assert.NoError(t, err) - assert.Equal(t, int64(1), counts) - - counts, err = user.GetStarredRepoCount(true) - assert.NoError(t, err) - assert.Equal(t, int64(2), counts) -} diff --git a/models/user.go b/models/user.go index 5e3ee3f601bb..4126606e2405 100644 --- a/models/user.go +++ b/models/user.go @@ -113,8 +113,6 @@ type User struct { LoginSource int64 `xorm:"NOT NULL DEFAULT 0"` LoginName string Type UserType - OwnedOrgs []*User `xorm:"-"` - Repos []*Repository `xorm:"-"` Location string Website string Rands string `xorm:"VARCHAR(10)"` @@ -219,16 +217,16 @@ func (u *User) SetLastLogin() { u.LastLoginUnix = timeutil.TimeStampNow() } -// UpdateDiffViewStyle updates the users diff view style -func (u *User) UpdateDiffViewStyle(style string) error { +// UpdateUserDiffViewStyle updates the users diff view style +func UpdateUserDiffViewStyle(u *User, style string) error { u.DiffViewStyle = style - return UpdateUserCols(u, "diff_view_style") + return UpdateUserCols(db.DefaultContext, u, "diff_view_style") } -// UpdateTheme updates a users' theme irrespective of the site wide theme -func (u *User) UpdateTheme(themeName string) error { +// UpdateUserTheme updates a users' theme irrespective of the site wide theme +func UpdateUserTheme(u *User, themeName string) error { u.Theme = themeName - return UpdateUserCols(u, "theme") + return UpdateUserCols(db.DefaultContext, u, "theme") } // GetEmail returns an noreply email, if the user has set to keep his @@ -256,12 +254,6 @@ func (u *User) IsOAuth2() bool { return u.LoginType == login.OAuth2 } -// HasForkedRepo checks if user has already forked a repository with given ID. -func (u *User) HasForkedRepo(repoID int64) bool { - _, has := HasForkedRepo(u.ID, repoID) - return has -} - // MaxCreationLimit returns the number of repositories a user is allowed to create func (u *User) MaxCreationLimit() int { if u.MaxRepoCreation <= -1 { @@ -337,8 +329,8 @@ func (u *User) GenerateEmailActivateCode(email string) string { return code } -// GetFollowers returns range of user's followers. -func (u *User) GetFollowers(listOptions db.ListOptions) ([]*User, error) { +// GetUserFollowers returns range of user's followers. +func GetUserFollowers(u *User, listOptions db.ListOptions) ([]*User, error) { sess := db.GetEngine(db.DefaultContext). Where("follow.follow_id=?", u.ID). Join("LEFT", "follow", "`user`.id=follow.user_id") @@ -354,13 +346,8 @@ func (u *User) GetFollowers(listOptions db.ListOptions) ([]*User, error) { return users, sess.Find(&users) } -// IsFollowing returns true if user is following followID. -func (u *User) IsFollowing(followID int64) bool { - return user_model.IsFollowing(u.ID, followID) -} - -// GetFollowing returns range of user's following. -func (u *User) GetFollowing(listOptions db.ListOptions) ([]*User, error) { +// GetUserFollowing returns range of user's following. +func GetUserFollowing(u *User, listOptions db.ListOptions) ([]*User, error) { sess := db.GetEngine(db.DefaultContext). Where("follow.user_id=?", u.ID). Join("LEFT", "follow", "`user`.id=follow.follow_id") @@ -442,12 +429,12 @@ func (u *User) IsPasswordSet() bool { return len(u.Passwd) != 0 } -// IsVisibleToUser check if viewer is able to see user profile -func (u *User) IsVisibleToUser(viewer *User) bool { - return u.isVisibleToUser(db.GetEngine(db.DefaultContext), viewer) +// IsUserVisibleToViewer check if viewer is able to see user profile +func IsUserVisibleToViewer(u *User, viewer *User) bool { + return isUserVisibleToViewer(db.GetEngine(db.DefaultContext), u, viewer) } -func (u *User) isVisibleToUser(e db.Engine, viewer *User) bool { +func isUserVisibleToViewer(e db.Engine, u *User, viewer *User) bool { if viewer != nil && viewer.IsAdmin { return true } @@ -503,26 +490,6 @@ func (u *User) IsOrganization() bool { return u.Type == UserTypeOrganization } -// IsUserOrgOwner returns true if user is in the owner team of given organization. -func (u *User) IsUserOrgOwner(orgID int64) bool { - isOwner, err := IsOrganizationOwner(orgID, u.ID) - if err != nil { - log.Error("IsOrganizationOwner: %v", err) - return false - } - return isOwner -} - -// IsPublicMember returns true if user public his/her membership in given organization. -func (u *User) IsPublicMember(orgID int64) bool { - isMember, err := IsPublicMembership(orgID, u.ID) - if err != nil { - log.Error("IsPublicMembership: %v", err) - return false - } - return isMember -} - // GetOrganizationCount returns count of membership of organization of the user. func GetOrganizationCount(ctx context.Context, u *User) (int64, error) { return db.GetEngine(ctx). @@ -530,17 +497,6 @@ func GetOrganizationCount(ctx context.Context, u *User) (int64, error) { Count(new(OrgUser)) } -// GetOrganizationCount returns count of membership of organization of user. -func (u *User) GetOrganizationCount() (int64, error) { - return GetOrganizationCount(db.DefaultContext, u) -} - -// GetRepositories returns repositories that user owns, including private repositories. -func (u *User) GetRepositories(listOpts db.ListOptions, names ...string) (err error) { - u.Repos, _, err = GetUserRepositories(&SearchRepoOptions{Actor: u, Private: true, ListOptions: listOpts, LowerNames: names}) - return err -} - // GetRepositoryIDs returns repositories IDs where user owned and has unittypes // Caller shall check that units is not globally disabled func (u *User) GetRepositoryIDs(units ...unit.Type) ([]int64, error) { @@ -644,17 +600,6 @@ func (u *User) GetActiveAccessRepoIDs(units ...unit.Type) ([]int64, error) { return append(ids, ids2...), nil } -// GetMirrorRepositories returns mirror repositories that user owns, including private repositories. -func (u *User) GetMirrorRepositories() ([]*Repository, error) { - return GetUserMirrorRepositories(u.ID) -} - -// GetOwnedOrganizations returns all organizations that user owns. -func (u *User) GetOwnedOrganizations() (err error) { - u.OwnedOrgs, err = GetOwnedOrgsByUserID(u.ID) - return err -} - // DisplayName returns full name if it's not empty, // returns username otherwise. func (u *User) DisplayName() string { @@ -714,9 +659,9 @@ func (u *User) EmailNotifications() string { } // SetEmailNotifications sets the user's email notification preference -func (u *User) SetEmailNotifications(set string) error { +func SetEmailNotifications(u *User, set string) error { u.EmailNotificationsPreference = set - if err := UpdateUserCols(u, "email_notifications_preference"); err != nil { + if err := UpdateUserCols(db.DefaultContext, u, "email_notifications_preference"); err != nil { log.Error("SetEmailNotifications: %v", err) return err } @@ -983,25 +928,6 @@ func VerifyUserActiveCode(code string) (user *User) { return nil } -// VerifyActiveEmailCode verifies active email code when active account -func VerifyActiveEmailCode(code, email string) *user_model.EmailAddress { - minutes := setting.Service.ActiveCodeLives - - if user := getVerifyUser(code); user != nil { - // time limit code - prefix := code[:base.TimeLimitCodeLength] - data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands) - - if base.VerifyTimeLimitCode(data, minutes, prefix) { - emailAddress := &user_model.EmailAddress{UID: user.ID, Email: email} - if has, _ := db.GetEngine(db.DefaultContext).Get(emailAddress); has { - return emailAddress - } - } - } - return nil -} - // ChangeUserName changes all corresponding setting from old user name to new one. func ChangeUserName(u *User, newUserName string) (err error) { oldUserName := u.Name @@ -1090,8 +1016,8 @@ func UpdateUser(u *User) error { } // UpdateUserCols update user according special columns -func UpdateUserCols(u *User, cols ...string) error { - return updateUserCols(db.GetEngine(db.DefaultContext), u, cols...) +func UpdateUserCols(ctx context.Context, u *User, cols ...string) error { + return updateUserCols(db.GetEngine(ctx), u, cols...) } func updateUserCols(e db.Engine, u *User, cols ...string) error { @@ -1228,14 +1154,6 @@ func DeleteUser(ctx context.Context, u *User) (err error) { if _, err = e.Delete(&PublicKey{OwnerID: u.ID}); err != nil { return fmt.Errorf("deletePublicKeys: %v", err) } - err = rewriteAllPublicKeys(e) - if err != nil { - return err - } - err = rewriteAllPrincipalKeys(e) - if err != nil { - return err - } // ***** END: PublicKey ***** // ***** START: GPGPublicKey ***** diff --git a/models/user_avatar.go b/models/user_avatar.go index 50998514e0ac..ae4cd1e5ec5b 100644 --- a/models/user_avatar.go +++ b/models/user_avatar.go @@ -25,11 +25,11 @@ func (u *User) CustomAvatarRelativePath() string { } // GenerateRandomAvatar generates a random avatar for user. -func (u *User) GenerateRandomAvatar() error { - return u.generateRandomAvatar(db.GetEngine(db.DefaultContext)) +func GenerateRandomAvatar(u *User) error { + return generateRandomAvatar(db.GetEngine(db.DefaultContext), u) } -func (u *User) generateRandomAvatar(e db.Engine) error { +func generateRandomAvatar(e db.Engine, u *User) error { seed := u.Email if len(seed) == 0 { seed = u.Name @@ -80,7 +80,7 @@ func (u *User) AvatarLinkWithSize(size int) string { if useLocalAvatar { if u.Avatar == "" && autoGenerateAvatar { - if err := u.GenerateRandomAvatar(); err != nil { + if err := GenerateRandomAvatar(u); err != nil { log.Error("GenerateRandomAvatar: %v", err) } } @@ -101,42 +101,6 @@ func (u *User) AvatarLink() string { return link } -// UploadAvatar saves custom avatar for user. -// FIXME: split uploads to different subdirs in case we have massive users. -func (u *User) UploadAvatar(data []byte) error { - m, err := avatar.Prepare(data) - if err != nil { - return err - } - - ctx, committer, err := db.TxContext() - if err != nil { - return err - } - defer committer.Close() - - u.UseCustomAvatar = true - // Different users can upload same image as avatar - // If we prefix it with u.ID, it will be separated - // Otherwise, if any of the users delete his avatar - // Other users will lose their avatars too. - u.Avatar = fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", u.ID, md5.Sum(data))))) - if err = updateUserCols(db.GetEngine(ctx), u, "use_custom_avatar", "avatar"); err != nil { - return fmt.Errorf("updateUser: %v", err) - } - - if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { - if err := png.Encode(w, *m); err != nil { - log.Error("Encode: %v", err) - } - return err - }); err != nil { - return fmt.Errorf("Failed to create dir %s: %v", u.CustomAvatarRelativePath(), err) - } - - return committer.Commit() -} - // IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data func (u *User) IsUploadAvatarChanged(data []byte) bool { if !u.UseCustomAvatar || len(u.Avatar) == 0 { @@ -145,21 +109,3 @@ func (u *User) IsUploadAvatarChanged(data []byte) bool { avatarID := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", u.ID, md5.Sum(data))))) return u.Avatar != avatarID } - -// DeleteAvatar deletes the user's custom avatar. -func (u *User) DeleteAvatar() error { - aPath := u.CustomAvatarRelativePath() - log.Trace("DeleteAvatar[%d]: %s", u.ID, aPath) - if len(u.Avatar) > 0 { - if err := storage.Avatars.Delete(aPath); err != nil { - return fmt.Errorf("Failed to remove %s: %v", aPath, err) - } - } - - u.UseCustomAvatar = false - u.Avatar = "" - if _, err := db.GetEngine(db.DefaultContext).ID(u.ID).Cols("avatar, use_custom_avatar").Update(u); err != nil { - return fmt.Errorf("UpdateUser: %v", err) - } - return nil -} diff --git a/models/user_email.go b/models/user_email.go index d9e63bfc5486..8c5fbf6d6aeb 100644 --- a/models/user_email.go +++ b/models/user_email.go @@ -10,6 +10,8 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "xorm.io/builder" @@ -93,6 +95,25 @@ func MakeEmailPrimary(email *user_model.EmailAddress) error { return committer.Commit() } +// VerifyActiveEmailCode verifies active email code when active account +func VerifyActiveEmailCode(code, email string) *user_model.EmailAddress { + minutes := setting.Service.ActiveCodeLives + + if user := getVerifyUser(code); user != nil { + // time limit code + prefix := code[:base.TimeLimitCodeLength] + data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands) + + if base.VerifyTimeLimitCode(data, minutes, prefix) { + emailAddress := &user_model.EmailAddress{UID: user.ID, Email: email} + if has, _ := db.GetEngine(db.DefaultContext).Get(emailAddress); has { + return emailAddress + } + } + } + return nil +} + // SearchEmailOrderBy is used to sort the results from SearchEmails() type SearchEmailOrderBy string diff --git a/models/user_test.go b/models/user_test.go index 7f29f5c849f3..cfbd84964096 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -52,7 +52,9 @@ func TestUserIsPublicMember(t *testing.T) { func testUserIsPublicMember(t *testing.T, uid, orgID int64, expected bool) { user, err := GetUserByID(uid) assert.NoError(t, err) - assert.Equal(t, expected, user.IsPublicMember(orgID)) + is, err := IsPublicMembership(orgID, user.ID) + assert.NoError(t, err) + assert.Equal(t, expected, is) } func TestIsUserOrgOwner(t *testing.T) { @@ -78,7 +80,9 @@ func TestIsUserOrgOwner(t *testing.T) { func testIsUserOrgOwner(t *testing.T, uid, orgID int64, expected bool) { user, err := GetUserByID(uid) assert.NoError(t, err) - assert.Equal(t, expected, user.IsUserOrgOwner(orgID)) + is, err := IsOrganizationOwner(orgID, user.ID) + assert.NoError(t, err) + assert.Equal(t, expected, is) } func TestGetUserEmailsByNames(t *testing.T) { @@ -198,13 +202,13 @@ func TestEmailNotificationPreferences(t *testing.T) { assert.Equal(t, test.expected, user.EmailNotifications()) // Try all possible settings - assert.NoError(t, user.SetEmailNotifications(EmailNotificationsEnabled)) + assert.NoError(t, SetEmailNotifications(user, EmailNotificationsEnabled)) assert.Equal(t, EmailNotificationsEnabled, user.EmailNotifications()) - assert.NoError(t, user.SetEmailNotifications(EmailNotificationsOnMention)) + assert.NoError(t, SetEmailNotifications(user, EmailNotificationsOnMention)) assert.Equal(t, EmailNotificationsOnMention, user.EmailNotifications()) - assert.NoError(t, user.SetEmailNotifications(EmailNotificationsDisabled)) + assert.NoError(t, SetEmailNotifications(user, EmailNotificationsDisabled)) assert.Equal(t, EmailNotificationsDisabled, user.EmailNotifications()) } } diff --git a/modules/context/org.go b/modules/context/org.go index 11521c452b3f..d4159a32a7ab 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -117,6 +117,10 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { } ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember + ctx.Data["IsPublicMember"] = func(uid int64) bool { + is, _ := models.IsPublicMembership(ctx.Org.Organization.ID, uid) + return is + } ctx.Data["CanCreateOrgRepo"] = ctx.Org.CanCreateOrgRepo ctx.Org.OrgLink = org.AsUser().OrganisationLink() diff --git a/modules/context/repo.go b/modules/context/repo.go index e3d66fc3b98a..c96d34f2fcc5 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -491,8 +491,8 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { ctx.Data["CanWriteIssues"] = ctx.Repo.CanWrite(unit_model.TypeIssues) ctx.Data["CanWritePulls"] = ctx.Repo.CanWrite(unit_model.TypePullRequests) - if ctx.Data["CanSignedUserFork"], err = ctx.Repo.Repository.CanUserFork(ctx.User); err != nil { - ctx.ServerError("CanUserFork", err) + if ctx.Data["CanSignedUserFork"], err = models.CanUserForkRepo(ctx.User, ctx.Repo.Repository); err != nil { + ctx.ServerError("CanSignedUserFork", err) return } @@ -577,7 +577,8 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) { ctx.Data["BranchName"] = ctx.Repo.BranchName // People who have push access or have forked repository can propose a new pull request. - canPush := ctx.Repo.CanWrite(unit_model.TypeCode) || (ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID)) + canPush := ctx.Repo.CanWrite(unit_model.TypeCode) || + (ctx.IsSigned && models.HasForkedRepo(ctx.User.ID, ctx.Repo.Repository.ID)) canCompare := false // Pull request is allowed if this is a fork repository diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index 4530349f2c20..d81832179050 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -190,7 +190,12 @@ func IsPublicMember(ctx *context.APIContext) { if ctx.Written() { return } - if userToCheck.IsPublicMember(ctx.Org.Organization.ID) { + is, err := models.IsPublicMembership(ctx.Org.Organization.ID, userToCheck.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "IsPublicMembership", err) + return + } + if is { ctx.Status(http.StatusNoContent) } else { ctx.NotFound() diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index f539662d6269..952e29fba9ce 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -9,6 +9,7 @@ import ( "net/http" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" @@ -343,7 +344,7 @@ func Edit(ctx *context.APIContext) { if form.RepoAdminChangeTeamAccess != nil { org.RepoAdminChangeTeamAccess = *form.RepoAdminChangeTeamAccess } - if err := models.UpdateUserCols(org.AsUser(), + if err := models.UpdateUserCols(db.DefaultContext, org.AsUser(), "full_name", "description", "website", "location", "visibility", "repo_admin_change_team_access", ); err != nil { diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 7f377edc3f8f..efe72e1502a2 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -953,10 +953,10 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) } // Check if current user has fork of repository or in the same repository. - headRepo, has := models.HasForkedRepo(headUser.ID, baseRepo.ID) - if !has && !isSameRepo { + headRepo := models.GetForkedRepo(headUser.ID, baseRepo.ID) + if headRepo == nil && !isSameRepo { log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) - ctx.NotFound("HasForkedRepo") + ctx.NotFound("GetForkedRepo") return nil, nil, nil, nil, "", "" } diff --git a/routers/api/v1/repo/star.go b/routers/api/v1/repo/star.go index 5fa42c3244e4..b8a54a6bdaf4 100644 --- a/routers/api/v1/repo/star.go +++ b/routers/api/v1/repo/star.go @@ -7,6 +7,7 @@ package repo import ( "net/http" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" @@ -43,7 +44,7 @@ func ListStargazers(ctx *context.APIContext) { // "200": // "$ref": "#/responses/UserList" - stargazers, err := ctx.Repo.Repository.GetStargazers(utils.GetListOptions(ctx)) + stargazers, err := models.GetStargazers(ctx.Repo.Repository, utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "GetStargazers", err) return diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 5d66f3bcc357..edf3cea3ea04 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -25,7 +25,7 @@ func responseAPIUsers(ctx *context.APIContext, users []*models.User) { } func listUserFollowers(ctx *context.APIContext, u *models.User) { - users, err := u.GetFollowers(utils.GetListOptions(ctx)) + users, err := models.GetUserFollowers(u, utils.GetListOptions(ctx)) if err != nil { ctx.Error(http.StatusInternalServerError, "GetUserFollowers", err) return @@ -91,9 +91,9 @@ func ListFollowers(ctx *context.APIContext) { } func listUserFollowing(ctx *context.APIContext, u *models.User) { - users, err := u.GetFollowing(utils.GetListOptions(ctx)) + users, err := models.GetUserFollowing(u, utils.GetListOptions(ctx)) if err != nil { - ctx.Error(http.StatusInternalServerError, "GetFollowing", err) + ctx.Error(http.StatusInternalServerError, "GetUserFollowing", err) return } @@ -157,7 +157,7 @@ func ListFollowing(ctx *context.APIContext) { } func checkUserFollowing(ctx *context.APIContext, u *models.User, followID int64) { - if u.IsFollowing(followID) { + if user_model.IsFollowing(u.ID, followID) { ctx.Status(http.StatusNoContent) } else { ctx.NotFound() diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 535a49d76648..8c57b1f099fa 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -103,7 +103,7 @@ func GetInfo(ctx *context.APIContext) { return } - if !u.IsVisibleToUser(ctx.User) { + if !models.IsUserVisibleToViewer(u, ctx.User) { // fake ErrUserNotExist error message to not leak information about existence ctx.NotFound("GetUserByName", models.ErrUserNotExist{Name: ctx.Params(":username")}) return diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index b23e4cf39b06..077cf02f15a7 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -23,10 +23,10 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/explore" - router_user_setting "code.gitea.io/gitea/routers/web/user/setting" + user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/mailer" - "code.gitea.io/gitea/services/user" + user_service "code.gitea.io/gitea/services/user" ) const ( @@ -309,7 +309,7 @@ func EditUserPost(ctx *context.Context) { } if len(form.UserName) != 0 && u.Name != form.UserName { - if err := router_user_setting.HandleUsernameChange(ctx, u, form.UserName); err != nil { + if err := user_setting.HandleUsernameChange(ctx, u, form.UserName); err != nil { ctx.Redirect(setting.AppSubURL + "/admin/users") return } @@ -378,7 +378,7 @@ func DeleteUser(ctx *context.Context) { return } - if err = user.DeleteUser(u); err != nil { + if err = user_service.DeleteUser(u); err != nil { switch { case models.IsErrUserOwnRepos(err): ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo")) @@ -411,7 +411,7 @@ func AvatarPost(ctx *context.Context) { } form := web.GetForm(ctx).(*forms.AvatarForm) - if err := router_user_setting.UpdateAvatarSetting(ctx, form, u); err != nil { + if err := user_setting.UpdateAvatarSetting(ctx, form, u); err != nil { ctx.Flash.Error(err.Error()) } else { ctx.Flash.Success(ctx.Tr("settings.update_user_avatar_success")) @@ -427,7 +427,7 @@ func DeleteAvatar(ctx *context.Context) { return } - if err := u.DeleteAvatar(); err != nil { + if err := user_service.DeleteAvatar(u); err != nil { ctx.Flash.Error(err.Error()) } diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 0752fa12cb93..6e8125713527 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -141,7 +141,6 @@ func Home(ctx *context.Context) { ctx.Data["MembersTotal"] = membersCount ctx.Data["Members"] = members ctx.Data["Teams"] = ctx.Org.Teams - ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5) diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index f05dbd8bc152..7a6f4fec60f5 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -18,9 +18,10 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" - userSetting "code.gitea.io/gitea/routers/web/user/setting" + user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/org" + user_service "code.gitea.io/gitea/services/user" ) const ( @@ -136,7 +137,7 @@ func SettingsPost(ctx *context.Context) { func SettingsAvatar(ctx *context.Context) { form := web.GetForm(ctx).(*forms.AvatarForm) form.Source = forms.AvatarLocal - if err := userSetting.UpdateAvatarSetting(ctx, form, ctx.Org.Organization.AsUser()); err != nil { + if err := user_setting.UpdateAvatarSetting(ctx, form, ctx.Org.Organization.AsUser()); err != nil { ctx.Flash.Error(err.Error()) } else { ctx.Flash.Success(ctx.Tr("org.settings.update_avatar_success")) @@ -147,7 +148,7 @@ func SettingsAvatar(ctx *context.Context) { // SettingsDeleteAvatar response for delete avatar on settings page func SettingsDeleteAvatar(ctx *context.Context) { - if err := ctx.Org.Organization.AsUser().DeleteAvatar(); err != nil { + if err := user_service.DeleteAvatar(ctx.Org.Organization.AsUser()); err != nil { ctx.Flash.Error(err.Error()) } diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 27cd06023d49..a6ad3eff5aa8 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -54,7 +54,8 @@ func Branches(ctx *context.Context) { ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls() ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode) ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror - ctx.Data["CanPull"] = ctx.Repo.CanWrite(unit.TypeCode) || (ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID)) + ctx.Data["CanPull"] = ctx.Repo.CanWrite(unit.TypeCode) || + (ctx.IsSigned && models.HasForkedRepo(ctx.User.ID, ctx.Repo.Repository.ID)) ctx.Data["PageIsViewCode"] = true ctx.Data["PageIsBranches"] = true diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index fdaf6fc6c231..706009820a4e 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -331,8 +331,8 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { // "OwnForkRepo" var ownForkRepo *models.Repository if ctx.User != nil && baseRepo.OwnerID != ctx.User.ID { - repo, has := models.HasForkedRepo(ctx.User.ID, baseRepo.ID) - if has { + repo := models.GetForkedRepo(ctx.User.ID, baseRepo.ID) + if repo != nil { ownForkRepo = repo ctx.Data["OwnForkRepo"] = ownForkRepo } @@ -355,12 +355,14 @@ func ParseCompareInfo(ctx *context.Context) *CompareInfo { // 5. If the headOwner has a fork of the baseRepo - use that if !has { - ci.HeadRepo, has = models.HasForkedRepo(ci.HeadUser.ID, baseRepo.ID) + ci.HeadRepo = models.GetForkedRepo(ci.HeadUser.ID, baseRepo.ID) + has = ci.HeadRepo != nil } // 6. If the baseRepo is a fork and the headUser has a fork of that use that if !has && baseRepo.IsFork { - ci.HeadRepo, has = models.HasForkedRepo(ci.HeadUser.ID, baseRepo.ForkID) + ci.HeadRepo = models.GetForkedRepo(ci.HeadUser.ID, baseRepo.ForkID) + has = ci.HeadRepo != nil } // 7. Otherwise if we're not the same repo and haven't found a repo give up diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 6cc9419763cd..ac6240e282dd 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -106,7 +106,7 @@ func MustAllowPulls(ctx *context.Context) { } // User can send pull request if owns a forked repository. - if ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID) { + if ctx.IsSigned && models.HasForkedRepo(ctx.User.ID, ctx.Repo.Repository.ID) { ctx.Repo.PullRequest.Allowed = true ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.User.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName) } diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go index a5d478dd73b3..0dc6e1cb7204 100644 --- a/routers/web/repo/middlewares.go +++ b/routers/web/repo/middlewares.go @@ -7,6 +7,7 @@ package repo import ( "fmt" + "code.gitea.io/gitea/models" admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" @@ -55,7 +56,7 @@ func SetDiffViewStyle(ctx *context.Context) { } ctx.Data["IsSplitStyle"] = style == "split" - if err := ctx.User.UpdateDiffViewStyle(style); err != nil { + if err := models.UpdateUserDiffViewStyle(ctx.User, style); err != nil { ctx.ServerError("ErrUpdateDiffViewStyle", err) } } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index a7afc3a05c11..83e353833fc7 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -108,23 +108,23 @@ func getForkRepository(ctx *context.Context) *models.Repository { ctx.Data["repo_name"] = forkRepo.Name ctx.Data["description"] = forkRepo.Description ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate - canForkToUser := forkRepo.OwnerID != ctx.User.ID && !ctx.User.HasForkedRepo(forkRepo.ID) + canForkToUser := forkRepo.OwnerID != ctx.User.ID && !models.HasForkedRepo(ctx.User.ID, forkRepo.ID) ctx.Data["ForkRepo"] = forkRepo - if err := ctx.User.GetOwnedOrganizations(); err != nil { - ctx.ServerError("GetOwnedOrganizations", err) + ownedOrgs, err := models.GetOwnedOrgsByUserID(ctx.User.ID) + if err != nil { + ctx.ServerError("GetOwnedOrgsByUserID", err) return nil } - var orgs []*models.User - for _, org := range ctx.User.OwnedOrgs { - if forkRepo.OwnerID != org.ID && !org.HasForkedRepo(forkRepo.ID) { + var orgs []*models.Organization + for _, org := range ownedOrgs { + if forkRepo.OwnerID != org.ID && !models.HasForkedRepo(org.ID, forkRepo.ID) { orgs = append(orgs, org) } } var traverseParentRepo = forkRepo - var err error for { if ctx.User.ID == traverseParentRepo.OwnerID { canForkToUser = false @@ -200,8 +200,8 @@ func ForkPost(ctx *context.Context) { ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) return } - repo, has := models.HasForkedRepo(ctxUser.ID, traverseParentRepo.ID) - if has { + repo := models.GetForkedRepo(ctxUser.ID, traverseParentRepo.ID) + if repo != nil { ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) return } diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index e8fdb99ff4bc..9463fc4c5f7a 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -63,7 +63,7 @@ func checkContextUser(ctx *context.Context, uid int64) *models.User { } if !ctx.User.IsAdmin { - orgsAvailable := []*models.User{} + orgsAvailable := []*models.Organization{} for i := 0; i < len(orgs); i++ { if orgs[i].CanCreateRepo() { orgsAvailable = append(orgsAvailable, orgs[i]) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 72726f0545f0..1293882cc5f1 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -928,7 +928,9 @@ func Stars(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.stargazers") ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers") ctx.Data["PageIsStargazers"] = true - RenderUserCards(ctx, ctx.Repo.Repository.NumStars, ctx.Repo.Repository.GetStargazers, tplWatchers) + RenderUserCards(ctx, ctx.Repo.Repository.NumStars, func(opts db.ListOptions) ([]*models.User, error) { + return models.GetStargazers(ctx.Repo.Repository, opts) + }, tplWatchers) } // Forks render repository's forked users diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go index 1b1c70c8fe9f..c5164c4956e1 100644 --- a/routers/web/user/auth.go +++ b/routers/web/user/auth.go @@ -33,6 +33,7 @@ import ( "code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/mailer" + user_service "code.gitea.io/gitea/services/user" "github.com/markbates/goth" "github.com/tstranex/u2f" @@ -564,7 +565,7 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR // If the user does not have a locale set, we save the current one. if len(u.Language) == 0 { u.Language = ctx.Locale.Language() - if err := models.UpdateUserCols(u, "language"); err != nil { + if err := models.UpdateUserCols(db.DefaultContext, u, "language"); err != nil { log.Error(fmt.Sprintf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language)) return setting.AppSubURL + "/" } @@ -581,7 +582,7 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR // Register last login u.SetLastLogin() - if err := models.UpdateUserCols(u, "last_login_unix"); err != nil { + if err := models.UpdateUserCols(db.DefaultContext, u, "last_login_unix"); err != nil { ctx.ServerError("UpdateUserCols", err) return setting.AppSubURL + "/" } @@ -736,7 +737,7 @@ func updateAvatarIfNeed(url string, u *models.User) { if err == nil && resp.StatusCode == http.StatusOK { data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1)) if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize { - _ = u.UploadAvatar(data) + _ = user_service.UploadAvatar(u, data) } } } @@ -773,7 +774,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *login.Source, u *models.Us // Register last login u.SetLastLogin() - if err := models.UpdateUserCols(u, "last_login_unix"); err != nil { + if err := models.UpdateUserCols(db.DefaultContext, u, "last_login_unix"); err != nil { ctx.ServerError("UpdateUserCols", err) return } @@ -1345,7 +1346,7 @@ func handleUserCreated(ctx *context.Context, u *models.User, gothUser *goth.User u.IsAdmin = true u.IsActive = true u.SetLastLogin() - if err := models.UpdateUserCols(u, "is_admin", "is_active", "last_login_unix"); err != nil { + if err := models.UpdateUserCols(db.DefaultContext, u, "is_admin", "is_active", "last_login_unix"); err != nil { ctx.ServerError("UpdateUser", err) return } @@ -1466,7 +1467,7 @@ func handleAccountActivation(ctx *context.Context, user *models.User) { ctx.ServerError("UpdateUser", err) return } - if err := models.UpdateUserCols(user, "is_active", "rands"); err != nil { + if err := models.UpdateUserCols(db.DefaultContext, user, "is_active", "rands"); err != nil { if models.IsErrUserNotExist(err) { ctx.NotFound("UpdateUserCols", err) } else { @@ -1726,7 +1727,7 @@ func ResetPasswdPost(ctx *context.Context) { return } u.MustChangePassword = false - if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil { + if err := models.UpdateUserCols(db.DefaultContext, u, "must_change_password", "passwd", "passwd_hash_algo", "rands", "salt"); err != nil { ctx.ServerError("UpdateUser", err) return } @@ -1802,7 +1803,7 @@ func MustChangePasswordPost(ctx *context.Context) { u.MustChangePassword = false - if err := models.UpdateUserCols(u, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil { + if err := models.UpdateUserCols(db.DefaultContext, u, "must_change_password", "passwd", "passwd_hash_algo", "salt"); err != nil { ctx.ServerError("UpdateUser", err) return } diff --git a/routers/web/user/home.go b/routers/web/user/home.go index c0ecd0c2a0f1..9efbe755d905 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -72,6 +72,8 @@ func Dashboard(ctx *context.Context) { ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard") ctx.Data["PageIsDashboard"] = true ctx.Data["PageIsNews"] = true + cnt, _ := models.GetOrganizationCount(db.DefaultContext, ctxUser) + ctx.Data["UserOrgsCount"] = cnt var uid int64 if ctxUser != nil { @@ -111,9 +113,9 @@ func Dashboard(ctx *context.Context) { return } } else { - mirrors, err = ctxUser.GetMirrorRepositories() + mirrors, err = models.GetUserMirrorRepositories(ctxUser.ID) if err != nil { - ctx.ServerError("GetMirrorRepositories", err) + ctx.ServerError("GetUserMirrorRepositories", err) return } } diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 9d0b4e3c15aa..2594ba3013b1 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -107,7 +107,7 @@ func Profile(ctx *context.Context) { } // check view permissions - if !ctxUser.IsVisibleToUser(ctx.User) { + if !models.IsUserVisibleToViewer(ctxUser, ctx.User) { ctx.NotFound("user", fmt.Errorf(uname)) return } @@ -137,10 +137,16 @@ func Profile(ctx *context.Context) { return } + var isFollowing bool + if ctx.User != nil && ctxUser != nil { + isFollowing = user_model.IsFollowing(ctx.User.ID, ctxUser.ID) + } + ctx.Data["Title"] = ctxUser.DisplayName() ctx.Data["PageIsUserProfile"] = true ctx.Data["Owner"] = ctxUser ctx.Data["OpenIDs"] = openIDs + ctx.Data["IsFollowing"] = isFollowing if setting.Service.EnableUserHeatmap { data, err := models.GetUserHeatmapDataByUser(ctxUser, ctx.User) @@ -227,24 +233,24 @@ func Profile(ctx *context.Context) { ctx.Data["Keyword"] = keyword switch tab { case "followers": - items, err := ctxUser.GetFollowers(db.ListOptions{ + items, err := models.GetUserFollowers(ctxUser, db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, Page: page, }) if err != nil { - ctx.ServerError("GetFollowers", err) + ctx.ServerError("GetUserFollowers", err) return } ctx.Data["Cards"] = items total = ctxUser.NumFollowers case "following": - items, err := ctxUser.GetFollowing(db.ListOptions{ + items, err := models.GetUserFollowing(ctxUser, db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, Page: page, }) if err != nil { - ctx.ServerError("GetFollowing", err) + ctx.ServerError("GetUserFollowing", err) return } ctx.Data["Cards"] = items diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 3362d3806d22..603786697aa0 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -11,6 +11,7 @@ import ( "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" @@ -74,7 +75,7 @@ func AccountPost(ctx *context.Context) { ctx.ServerError("UpdateUser", err) return } - if err := models.UpdateUserCols(ctx.User, "salt", "passwd_hash_algo", "passwd"); err != nil { + if err := models.UpdateUserCols(db.DefaultContext, ctx.User, "salt", "passwd_hash_algo", "passwd"); err != nil { ctx.ServerError("UpdateUser", err) return } @@ -158,7 +159,7 @@ func EmailPost(ctx *context.Context) { ctx.ServerError("SetEmailPreference", errors.New("option unrecognized")) return } - if err := ctx.User.SetEmailNotifications(preference); err != nil { + if err := models.SetEmailNotifications(ctx.User, preference); err != nil { log.Error("Set Email Notifications failed: %v", err) ctx.ServerError("SetEmailNotifications", err) return diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 36fe45df04ff..1e515f2fd311 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -27,6 +27,7 @@ import ( "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/agit" "code.gitea.io/gitea/services/forms" + user_service "code.gitea.io/gitea/services/user" "github.com/unknwon/i18n" ) @@ -171,18 +172,18 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser * if !(st.IsImage() && !st.IsSvgImage()) { return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) } - if err = ctxUser.UploadAvatar(data); err != nil { + if err = user_service.UploadAvatar(ctxUser, data); err != nil { return fmt.Errorf("UploadAvatar: %v", err) } } else if ctxUser.UseCustomAvatar && ctxUser.Avatar == "" { // No avatar is uploaded but setting has been changed to enable, // generate a random one when needed. - if err := ctxUser.GenerateRandomAvatar(); err != nil { + if err := models.GenerateRandomAvatar(ctxUser); err != nil { log.Error("GenerateRandomAvatar[%d]: %v", ctxUser.ID, err) } } - if err := models.UpdateUserCols(ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil { + if err := models.UpdateUserCols(db.DefaultContext, ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil { return fmt.Errorf("UpdateUser: %v", err) } @@ -203,7 +204,7 @@ func AvatarPost(ctx *context.Context) { // DeleteAvatar render delete avatar page func DeleteAvatar(ctx *context.Context) { - if err := ctx.User.DeleteAvatar(); err != nil { + if err := user_service.DeleteAvatar(ctx.User); err != nil { ctx.Flash.Error(err.Error()) } @@ -301,11 +302,20 @@ func Repos(ctx *context.Context) { return } - if err := ctxUser.GetRepositories(db.ListOptions{Page: 1, PageSize: setting.UI.Admin.UserPagingNum}, repoNames...); err != nil { - ctx.ServerError("GetRepositories", err) + userRepos, _, err := models.GetUserRepositories(&models.SearchRepoOptions{ + Actor: ctxUser, + Private: true, + ListOptions: db.ListOptions{ + Page: 1, + PageSize: setting.UI.Admin.UserPagingNum, + }, + LowerNames: repoNames, + }) + if err != nil { + ctx.ServerError("GetUserRepositories", err) return } - for _, repo := range ctxUser.Repos { + for _, repo := range userRepos { if repo.IsFork { if err := repo.GetBaseRepo(); err != nil { ctx.ServerError("GetBaseRepo", err) @@ -317,16 +327,12 @@ func Repos(ctx *context.Context) { ctx.Data["Dirs"] = repoNames ctx.Data["ReposMap"] = repos } else { - var err error - var count64 int64 - ctxUser.Repos, count64, err = models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts}) - + repos, count64, err := models.GetUserRepositories(&models.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts}) if err != nil { - ctx.ServerError("GetRepositories", err) + ctx.ServerError("GetUserRepositories", err) return } count = int(count64) - repos := ctxUser.Repos for i := range repos { if repos[i].IsFork { @@ -371,7 +377,7 @@ func UpdateUIThemePost(ctx *context.Context) { return } - if err := ctx.User.UpdateTheme(form.Theme); err != nil { + if err := models.UpdateUserTheme(ctx.User, form.Theme); err != nil { ctx.Flash.Error(ctx.Tr("settings.theme_update_error")) ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") return diff --git a/services/auth/auth.go b/services/auth/auth.go index eb78cfdccedb..3e48e1504796 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -13,6 +13,7 @@ import ( "strings" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" @@ -127,7 +128,7 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore if len(user.Language) == 0 { lc := middleware.Locale(resp, req) user.Language = lc.Language() - if err := models.UpdateUserCols(user, "language"); err != nil { + if err := models.UpdateUserCols(db.DefaultContext, user, "language"); err != nil { log.Error(fmt.Sprintf("Error updating user language [user: %d, locale: %s]", user.ID, user.Language)) return } diff --git a/services/auth/source/db/authenticate.go b/services/auth/source/db/authenticate.go index e73ab15d2835..af7b719a634e 100644 --- a/services/auth/source/db/authenticate.go +++ b/services/auth/source/db/authenticate.go @@ -6,6 +6,7 @@ package db import ( "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/setting" ) @@ -24,7 +25,7 @@ func Authenticate(user *models.User, login, password string) (*models.User, erro if err := user.SetPassword(password); err != nil { return nil, err } - if err := models.UpdateUserCols(user, "passwd", "passwd_hash_algo", "salt"); err != nil { + if err := models.UpdateUserCols(db.DefaultContext, user, "passwd", "passwd_hash_algo", "salt"); err != nil { return nil, err } } diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index 2719b5b7154f..99a99801a4bf 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -9,8 +9,10 @@ import ( "strings" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/login" "code.gitea.io/gitea/services/mailer" + user_service "code.gitea.io/gitea/services/user" ) // Authenticate queries if login/password is valid against the LDAP directory pool, @@ -47,7 +49,7 @@ func (source *Source) Authenticate(user *models.User, userName, password string) cols = append(cols, "is_restricted") } if len(cols) > 0 { - err = models.UpdateUserCols(user, cols...) + err = models.UpdateUserCols(db.DefaultContext, user, cols...) if err != nil { return nil, err } @@ -97,7 +99,7 @@ func (source *Source) Authenticate(user *models.User, userName, password string) } if err == nil && len(source.AttributeAvatar) > 0 { - _ = user.UploadAvatar(sr.Avatar) + _ = user_service.UploadAvatar(user, sr.Avatar) } return user, err diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index 4508d1514ae0..89f84ae20cfc 100644 --- a/services/auth/source/ldap/source_sync.go +++ b/services/auth/source/ldap/source_sync.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" + user_service "code.gitea.io/gitea/services/user" ) // Sync causes this ldap source to synchronize its users with the db @@ -123,7 +124,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { } if err == nil && len(source.AttributeAvatar) > 0 { - _ = usr.UploadAvatar(su.Avatar) + _ = user_service.UploadAvatar(usr, su.Avatar) } } else if updateExisting { // Synchronize SSH Public Key if that attribute is set @@ -152,7 +153,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { } usr.IsActive = true - err = models.UpdateUserCols(usr, "full_name", "email", "is_admin", "is_restricted", "is_active") + err = models.UpdateUserCols(db.DefaultContext, usr, "full_name", "email", "is_admin", "is_restricted", "is_active") if err != nil { log.Error("SyncExternalUsers[%s]: Error updating user %s: %v", source.loginSource.Name, usr.Name, err) } @@ -160,7 +161,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { if usr.IsUploadAvatarChanged(su.Avatar) { if err == nil && len(source.AttributeAvatar) > 0 { - _ = usr.UploadAvatar(su.Avatar) + _ = user_service.UploadAvatar(usr, su.Avatar) } } @@ -193,7 +194,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { log.Trace("SyncExternalUsers[%s]: Deactivating user %s", source.loginSource.Name, usr.Name) usr.IsActive = false - err = models.UpdateUserCols(usr, "is_active") + err = models.UpdateUserCols(db.DefaultContext, usr, "is_active") if err != nil { log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", source.loginSource.Name, usr.Name, err) } diff --git a/services/user/user.go b/services/user/user.go index 733cc4a36e88..0578f70b27f8 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -6,13 +6,18 @@ package user import ( "context" + "crypto/md5" "fmt" + "image/png" + "io" "time" "code.gitea.io/gitea/models" admin_model "code.gitea.io/gitea/models/admin" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/avatar" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/util" ) @@ -58,6 +63,13 @@ func DeleteUser(u *models.User) error { return err } + if err = models.RewriteAllPublicKeys(); err != nil { + return err + } + if err = models.RewriteAllPrincipalKeys(); err != nil { + return err + } + // Note: There are something just cannot be roll back, // so just keep error logs of those operations. path := models.UserPath(u.Name) @@ -104,3 +116,56 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error { return user_model.DeleteInactiveEmailAddresses(ctx) } + +// UploadAvatar saves custom avatar for user. +func UploadAvatar(u *models.User, data []byte) error { + m, err := avatar.Prepare(data) + if err != nil { + return err + } + + ctx, committer, err := db.TxContext() + if err != nil { + return err + } + defer committer.Close() + + u.UseCustomAvatar = true + // Different users can upload same image as avatar + // If we prefix it with u.ID, it will be separated + // Otherwise, if any of the users delete his avatar + // Other users will lose their avatars too. + u.Avatar = fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", u.ID, md5.Sum(data))))) + if err = models.UpdateUserCols(ctx, u, "use_custom_avatar", "avatar"); err != nil { + return fmt.Errorf("updateUser: %v", err) + } + + if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { + if err := png.Encode(w, *m); err != nil { + log.Error("Encode: %v", err) + } + return err + }); err != nil { + return fmt.Errorf("Failed to create dir %s: %v", u.CustomAvatarRelativePath(), err) + } + + return committer.Commit() +} + +// DeleteAvatar deletes the user's custom avatar. +func DeleteAvatar(u *models.User) error { + aPath := u.CustomAvatarRelativePath() + log.Trace("DeleteAvatar[%d]: %s", u.ID, aPath) + if len(u.Avatar) > 0 { + if err := storage.Avatars.Delete(aPath); err != nil { + return fmt.Errorf("Failed to remove %s: %v", aPath, err) + } + } + + u.UseCustomAvatar = false + u.Avatar = "" + if _, err := db.GetEngine(db.DefaultContext).ID(u.ID).Cols("avatar, use_custom_avatar").Update(u); err != nil { + return fmt.Errorf("UpdateUser: %v", err) + } + return nil +} diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index 868d956011e9..fc79deb1399a 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -50,7 +50,7 @@
{{$isMember := .IsOrganizationMember}} {{range .Members}} - {{if or $isMember (.IsPublicMember $.Org.ID)}} + {{if or $isMember (call $.IsPublicMember .ID)}} {{avatar .}} diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl index 5791be9c0fb8..41230d3287e2 100644 --- a/templates/user/dashboard/repolist.tmpl +++ b/templates/user/dashboard/repolist.tmpl @@ -14,7 +14,7 @@ {{end}} ]" :is-organization="false" - :organizations-total-count="{{.ContextUser.GetOrganizationCount}}" + :organizations-total-count="{{.UserOrgsCount}}" :can-create-organization="{{.SignedUser.CanCreateOrganization}}" {{end}} inline-template diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 1c40bd21b557..3bb15449a2db 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -65,7 +65,7 @@ {{end}} {{if and .IsSigned (ne .SignedUserName .Owner.Name)}}