mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Add Visible modes function from Organisation to Users too (#16069)
You can limit or hide organisations. This pull make it also posible for users - new strings to translte - add checkbox to user profile form - add checkbox to admin user.edit form - filter explore page user search - filter api admin and public user searches - allow admins view "hidden" users - add app option DEFAULT_USER_VISIBILITY - rewrite many files to use Visibility field - check for teams intersection - fix context output - right fake 404 if not visible Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
		 Sergey Dryabzhinsky
					Sergey Dryabzhinsky
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							19ac575d57
						
					
				
				
					commit
					22a0636544
				
			| @@ -651,9 +651,15 @@ PATH = | |||||||
| ;DEFAULT_ALLOW_CREATE_ORGANIZATION = true | ;DEFAULT_ALLOW_CREATE_ORGANIZATION = true | ||||||
| ;; | ;; | ||||||
| ;; Either "public", "limited" or "private", default is "public" | ;; Either "public", "limited" or "private", default is "public" | ||||||
| ;; Limited is for signed user only | ;; Limited is for users visible only to signed users | ||||||
| ;; Private is only for member of the organization | ;; Private is for users visible only to members of their organizations | ||||||
| ;; Public is for everyone | ;; Public is for users visible for everyone | ||||||
|  | ;DEFAULT_USER_VISIBILITY = public | ||||||
|  | ;; | ||||||
|  | ;; Either "public", "limited" or "private", default is "public" | ||||||
|  | ;; Limited is for organizations visible only to signed users | ||||||
|  | ;; Private is for organizations visible only to members of the organization | ||||||
|  | ;; Public is for organizations visible to everyone | ||||||
| ;DEFAULT_ORG_VISIBILITY = public | ;DEFAULT_ORG_VISIBILITY = public | ||||||
| ;; | ;; | ||||||
| ;; Default value for DefaultOrgMemberVisible | ;; Default value for DefaultOrgMemberVisible | ||||||
|   | |||||||
| @@ -512,6 +512,7 @@ relation to port exhaustion. | |||||||
| - `SHOW_MILESTONES_DASHBOARD_PAGE`: **true** Enable this to show the milestones dashboard page - a view of all the user's milestones | - `SHOW_MILESTONES_DASHBOARD_PAGE`: **true** Enable this to show the milestones dashboard page - a view of all the user's milestones | ||||||
| - `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created | - `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created | ||||||
| - `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it | - `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it | ||||||
|  | - `DEFAULT_USER_VISIBILITY`: **public**: Set default visibility mode for users, either "public", "limited" or "private". | ||||||
| - `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private". | - `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private". | ||||||
| - `DEFAULT_ORG_MEMBER_VISIBLE`: **false** True will make the membership of the users visible when added to the organisation. | - `DEFAULT_ORG_MEMBER_VISIBLE`: **false** True will make the membership of the users visible when added to the organisation. | ||||||
| - `ALLOW_ONLY_INTERNAL_REGISTRATION`: **false** Set to true to force registration only via gitea. | - `ALLOW_ONLY_INTERNAL_REGISTRATION`: **false** Set to true to force registration only via gitea. | ||||||
|   | |||||||
| @@ -59,3 +59,34 @@ func TestAPIUserSearchNotLoggedIn(t *testing.T) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestAPIUserSearchAdminLoggedInUserHidden(t *testing.T) { | ||||||
|  | 	defer prepareTestEnv(t)() | ||||||
|  | 	adminUsername := "user1" | ||||||
|  | 	session := loginUser(t, adminUsername) | ||||||
|  | 	token := getTokenForLoggedInUser(t, session) | ||||||
|  | 	query := "user31" | ||||||
|  | 	req := NewRequestf(t, "GET", "/api/v1/users/search?token=%s&q=%s", token, query) | ||||||
|  | 	req.SetBasicAuth(token, "x-oauth-basic") | ||||||
|  | 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||||
|  |  | ||||||
|  | 	var results SearchResults | ||||||
|  | 	DecodeJSON(t, resp, &results) | ||||||
|  | 	assert.NotEmpty(t, results.Data) | ||||||
|  | 	for _, user := range results.Data { | ||||||
|  | 		assert.Contains(t, user.UserName, query) | ||||||
|  | 		assert.NotEmpty(t, user.Email) | ||||||
|  | 		assert.EqualValues(t, "private", user.Visibility) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAPIUserSearchNotLoggedInUserHidden(t *testing.T) { | ||||||
|  | 	defer prepareTestEnv(t)() | ||||||
|  | 	query := "user31" | ||||||
|  | 	req := NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query) | ||||||
|  | 	resp := MakeRequest(t, req, http.StatusOK) | ||||||
|  |  | ||||||
|  | 	var results SearchResults | ||||||
|  | 	DecodeJSON(t, resp, &results) | ||||||
|  | 	assert.Empty(t, results.Data) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -508,7 +508,6 @@ | |||||||
|   num_repos: 0 |   num_repos: 0 | ||||||
|   is_active: true |   is_active: true | ||||||
|  |  | ||||||
|  |  | ||||||
| - | - | ||||||
|   id: 30 |   id: 30 | ||||||
|   lower_name: user30 |   lower_name: user30 | ||||||
| @@ -525,3 +524,20 @@ | |||||||
|   avatar_email: user30@example.com |   avatar_email: user30@example.com | ||||||
|   num_repos: 2 |   num_repos: 2 | ||||||
|   is_active: true |   is_active: true | ||||||
|  |  | ||||||
|  | - | ||||||
|  |   id: 31 | ||||||
|  |   lower_name: user31 | ||||||
|  |   name: user31 | ||||||
|  |   full_name: "user31" | ||||||
|  |   email: user31@example.com | ||||||
|  |   passwd_hash_algo: argon2 | ||||||
|  |   passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password | ||||||
|  |   type: 0 # individual | ||||||
|  |   salt: ZogKvWdyEx | ||||||
|  |   is_admin: false | ||||||
|  |   visibility: 2 | ||||||
|  |   avatar: avatar31 | ||||||
|  |   avatar_email: user31@example.com | ||||||
|  |   num_repos: 0 | ||||||
|  |   is_active: true | ||||||
|   | |||||||
| @@ -455,22 +455,22 @@ func getOwnedOrgsByUserID(sess *xorm.Session, userID int64) ([]*User, error) { | |||||||
| 		Find(&orgs) | 		Find(&orgs) | ||||||
| } | } | ||||||
|  |  | ||||||
| // HasOrgVisible tells if the given user can see the given org | // HasOrgOrUserVisible tells if the given user can see the given org or user | ||||||
| func HasOrgVisible(org, user *User) bool { | func HasOrgOrUserVisible(org, user *User) bool { | ||||||
| 	return hasOrgVisible(x, org, user) | 	return hasOrgOrUserVisible(x, org, user) | ||||||
| } | } | ||||||
|  |  | ||||||
| func hasOrgVisible(e Engine, org, user *User) bool { | func hasOrgOrUserVisible(e Engine, orgOrUser, user *User) bool { | ||||||
| 	// Not SignedUser | 	// Not SignedUser | ||||||
| 	if user == nil { | 	if user == nil { | ||||||
| 		return org.Visibility == structs.VisibleTypePublic | 		return orgOrUser.Visibility == structs.VisibleTypePublic | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if user.IsAdmin { | 	if user.IsAdmin || orgOrUser.ID == user.ID { | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (org.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !org.hasMemberWithUserID(e, user.ID) { | 	if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !orgOrUser.hasMemberWithUserID(e, user.ID) { | ||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
| 	return true | 	return true | ||||||
| @@ -483,7 +483,7 @@ func HasOrgsVisible(orgs []*User, user *User) bool { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, org := range orgs { | 	for _, org := range orgs { | ||||||
| 		if HasOrgVisible(org, user) { | 		if HasOrgOrUserVisible(org, user) { | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -586,9 +586,9 @@ func TestHasOrgVisibleTypePublic(t *testing.T) { | |||||||
| 	assert.NoError(t, CreateOrganization(org, owner)) | 	assert.NoError(t, CreateOrganization(org, owner)) | ||||||
| 	org = AssertExistsAndLoadBean(t, | 	org = AssertExistsAndLoadBean(t, | ||||||
| 		&User{Name: org.Name, Type: UserTypeOrganization}).(*User) | 		&User{Name: org.Name, Type: UserTypeOrganization}).(*User) | ||||||
| 	test1 := HasOrgVisible(org, owner) | 	test1 := HasOrgOrUserVisible(org, owner) | ||||||
| 	test2 := HasOrgVisible(org, user3) | 	test2 := HasOrgOrUserVisible(org, user3) | ||||||
| 	test3 := HasOrgVisible(org, nil) | 	test3 := HasOrgOrUserVisible(org, nil) | ||||||
| 	assert.True(t, test1) // owner of org | 	assert.True(t, test1) // owner of org | ||||||
| 	assert.True(t, test2) // user not a part of org | 	assert.True(t, test2) // user not a part of org | ||||||
| 	assert.True(t, test3) // logged out user | 	assert.True(t, test3) // logged out user | ||||||
| @@ -609,9 +609,9 @@ func TestHasOrgVisibleTypeLimited(t *testing.T) { | |||||||
| 	assert.NoError(t, CreateOrganization(org, owner)) | 	assert.NoError(t, CreateOrganization(org, owner)) | ||||||
| 	org = AssertExistsAndLoadBean(t, | 	org = AssertExistsAndLoadBean(t, | ||||||
| 		&User{Name: org.Name, Type: UserTypeOrganization}).(*User) | 		&User{Name: org.Name, Type: UserTypeOrganization}).(*User) | ||||||
| 	test1 := HasOrgVisible(org, owner) | 	test1 := HasOrgOrUserVisible(org, owner) | ||||||
| 	test2 := HasOrgVisible(org, user3) | 	test2 := HasOrgOrUserVisible(org, user3) | ||||||
| 	test3 := HasOrgVisible(org, nil) | 	test3 := HasOrgOrUserVisible(org, nil) | ||||||
| 	assert.True(t, test1)  // owner of org | 	assert.True(t, test1)  // owner of org | ||||||
| 	assert.True(t, test2)  // user not a part of org | 	assert.True(t, test2)  // user not a part of org | ||||||
| 	assert.False(t, test3) // logged out user | 	assert.False(t, test3) // logged out user | ||||||
| @@ -632,9 +632,9 @@ func TestHasOrgVisibleTypePrivate(t *testing.T) { | |||||||
| 	assert.NoError(t, CreateOrganization(org, owner)) | 	assert.NoError(t, CreateOrganization(org, owner)) | ||||||
| 	org = AssertExistsAndLoadBean(t, | 	org = AssertExistsAndLoadBean(t, | ||||||
| 		&User{Name: org.Name, Type: UserTypeOrganization}).(*User) | 		&User{Name: org.Name, Type: UserTypeOrganization}).(*User) | ||||||
| 	test1 := HasOrgVisible(org, owner) | 	test1 := HasOrgOrUserVisible(org, owner) | ||||||
| 	test2 := HasOrgVisible(org, user3) | 	test2 := HasOrgOrUserVisible(org, user3) | ||||||
| 	test3 := HasOrgVisible(org, nil) | 	test3 := HasOrgOrUserVisible(org, nil) | ||||||
| 	assert.True(t, test1)  // owner of org | 	assert.True(t, test1)  // owner of org | ||||||
| 	assert.False(t, test2) // user not a part of org | 	assert.False(t, test2) // user not a part of org | ||||||
| 	assert.False(t, test3) // logged out user | 	assert.False(t, test3) // logged out user | ||||||
|   | |||||||
| @@ -585,8 +585,7 @@ func (repo *Repository) getReviewers(e Engine, doerID, posterID int64) ([]*User, | |||||||
|  |  | ||||||
| 	var users []*User | 	var users []*User | ||||||
|  |  | ||||||
| 	if repo.IsPrivate || | 	if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate { | ||||||
| 		(repo.Owner.IsOrganization() && repo.Owner.Visibility == api.VisibleTypePrivate) { |  | ||||||
| 		// This a private repository: | 		// This a private repository: | ||||||
| 		// Anyone who can read the repository is a requestable reviewer | 		// Anyone who can read the repository is a requestable reviewer | ||||||
| 		if err := e. | 		if err := e. | ||||||
|   | |||||||
| @@ -176,9 +176,9 @@ func getUserRepoPermission(e Engine, repo *Repository, user *User) (perm Permiss | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Prevent strangers from checking out public repo of private orginization | 	// Prevent strangers from checking out public repo of private orginization/users | ||||||
| 	// Allow user if they are collaborator of a repo within a private orginization but not a member of the orginization itself | 	// Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself | ||||||
| 	if repo.Owner.IsOrganization() && !hasOrgVisible(e, repo.Owner, user) && !isCollaborator { | 	if !hasOrgOrUserVisible(e, repo.Owner, user) && !isCollaborator { | ||||||
| 		perm.AccessMode = AccessModeNone | 		perm.AccessMode = AccessModeNone | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -432,6 +432,62 @@ func (u *User) IsPasswordSet() bool { | |||||||
| 	return len(u.Passwd) != 0 | 	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(x, viewer) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (u *User) isVisibleToUser(e Engine, viewer *User) bool { | ||||||
|  | 	if viewer != nil && viewer.IsAdmin { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch u.Visibility { | ||||||
|  | 	case structs.VisibleTypePublic: | ||||||
|  | 		return true | ||||||
|  | 	case structs.VisibleTypeLimited: | ||||||
|  | 		if viewer == nil || viewer.IsRestricted { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 		return true | ||||||
|  | 	case structs.VisibleTypePrivate: | ||||||
|  | 		if viewer == nil || viewer.IsRestricted { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// If they follow - they see each over | ||||||
|  | 		follower := IsFollowing(u.ID, viewer.ID) | ||||||
|  | 		if follower { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Now we need to check if they in some organization together | ||||||
|  | 		count, err := x.Table("team_user"). | ||||||
|  | 			Where( | ||||||
|  | 				builder.And( | ||||||
|  | 					builder.Eq{"uid": viewer.ID}, | ||||||
|  | 					builder.Or( | ||||||
|  | 						builder.Eq{"org_id": u.ID}, | ||||||
|  | 						builder.In("org_id", | ||||||
|  | 							builder.Select("org_id"). | ||||||
|  | 								From("team_user", "t2"). | ||||||
|  | 								Where(builder.Eq{"uid": u.ID}))))). | ||||||
|  | 			Count(new(TeamUser)) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if count < 0 { | ||||||
|  | 			// No common organization | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// they are in an organization together | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
| // IsOrganization returns true if user is actually a organization. | // IsOrganization returns true if user is actually a organization. | ||||||
| func (u *User) IsOrganization() bool { | func (u *User) IsOrganization() bool { | ||||||
| 	return u.Type == UserTypeOrganization | 	return u.Type == UserTypeOrganization | ||||||
| @@ -796,8 +852,13 @@ func IsUsableUsername(name string) error { | |||||||
| 	return isUsableName(reservedUsernames, reservedUserPatterns, name) | 	return isUsableName(reservedUsernames, reservedUserPatterns, name) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // CreateUserOverwriteOptions are an optional options who overwrite system defaults on user creation | ||||||
|  | type CreateUserOverwriteOptions struct { | ||||||
|  | 	Visibility structs.VisibleType | ||||||
|  | } | ||||||
|  |  | ||||||
| // CreateUser creates record of a new user. | // CreateUser creates record of a new user. | ||||||
| func CreateUser(u *User) (err error) { | func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { | ||||||
| 	if err = IsUsableUsername(u.Name); err != nil { | 	if err = IsUsableUsername(u.Name); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @@ -831,8 +892,6 @@ func CreateUser(u *User) (err error) { | |||||||
| 		return ErrEmailAlreadyUsed{u.Email} | 		return ErrEmailAlreadyUsed{u.Email} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate |  | ||||||
|  |  | ||||||
| 	u.LowerName = strings.ToLower(u.Name) | 	u.LowerName = strings.ToLower(u.Name) | ||||||
| 	u.AvatarEmail = u.Email | 	u.AvatarEmail = u.Email | ||||||
| 	if u.Rands, err = GetUserSalt(); err != nil { | 	if u.Rands, err = GetUserSalt(); err != nil { | ||||||
| @@ -841,10 +900,18 @@ func CreateUser(u *User) (err error) { | |||||||
| 	if err = u.SetPassword(u.Passwd); err != nil { | 	if err = u.SetPassword(u.Passwd); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// set system defaults | ||||||
|  | 	u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate | ||||||
|  | 	u.Visibility = setting.Service.DefaultUserVisibilityMode | ||||||
| 	u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation | 	u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation | ||||||
| 	u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification | 	u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification | ||||||
| 	u.MaxRepoCreation = -1 | 	u.MaxRepoCreation = -1 | ||||||
| 	u.Theme = setting.UI.DefaultTheme | 	u.Theme = setting.UI.DefaultTheme | ||||||
|  | 	// overwrite defaults if set | ||||||
|  | 	if len(overwriteDefault) != 0 && overwriteDefault[0] != nil { | ||||||
|  | 		u.Visibility = overwriteDefault[0].Visibility | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if _, err = sess.Insert(u); err != nil { | 	if _, err = sess.Insert(u); err != nil { | ||||||
| 		return err | 		return err | ||||||
| @@ -1527,10 +1594,9 @@ func (opts *SearchUserOptions) toConds() builder.Cond { | |||||||
| 		cond = cond.And(keywordCond) | 		cond = cond.And(keywordCond) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// If visibility filtered | ||||||
| 	if len(opts.Visible) > 0 { | 	if len(opts.Visible) > 0 { | ||||||
| 		cond = cond.And(builder.In("visibility", opts.Visible)) | 		cond = cond.And(builder.In("visibility", opts.Visible)) | ||||||
| 	} else { |  | ||||||
| 		cond = cond.And(builder.In("visibility", structs.VisibleTypePublic)) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if opts.Actor != nil { | 	if opts.Actor != nil { | ||||||
| @@ -1543,6 +1609,9 @@ func (opts *SearchUserOptions) toConds() builder.Cond { | |||||||
| 			exprCond = builder.Expr("org_user.org_id = \"user\".id") | 			exprCond = builder.Expr("org_user.org_id = \"user\".id") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// If Admin - they see all users! | ||||||
|  | 		if !opts.Actor.IsAdmin { | ||||||
|  | 			// Force visiblity for privacy | ||||||
| 			var accessCond builder.Cond | 			var accessCond builder.Cond | ||||||
| 			if !opts.Actor.IsRestricted { | 			if !opts.Actor.IsRestricted { | ||||||
| 				accessCond = builder.Or( | 				accessCond = builder.Or( | ||||||
| @@ -1552,9 +1621,17 @@ func (opts *SearchUserOptions) toConds() builder.Cond { | |||||||
| 				// restricted users only see orgs they are a member of | 				// restricted users only see orgs they are a member of | ||||||
| 				accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}))) | 				accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}))) | ||||||
| 			} | 			} | ||||||
|  | 			// Don't forget about self | ||||||
|  | 			accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID}) | ||||||
| 			cond = cond.And(accessCond) | 			cond = cond.And(accessCond) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 	} else { | ||||||
|  | 		// Force visiblity for privacy | ||||||
|  | 		// Not logged in - only public users | ||||||
|  | 		cond = cond.And(builder.In("visibility", structs.VisibleTypePublic)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if opts.UID > 0 { | 	if opts.UID > 0 { | ||||||
| 		cond = cond.And(builder.Eq{"id": opts.UID}) | 		cond = cond.And(builder.Eq{"id": opts.UID}) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -62,10 +62,14 @@ func toUser(user *models.User, signed, authed bool) *api.User { | |||||||
| 		Following:    user.NumFollowing, | 		Following:    user.NumFollowing, | ||||||
| 		StarredRepos: user.NumStars, | 		StarredRepos: user.NumStars, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	result.Visibility = user.Visibility.String() | ||||||
|  |  | ||||||
| 	// hide primary email if API caller is anonymous or user keep email private | 	// hide primary email if API caller is anonymous or user keep email private | ||||||
| 	if signed && (!user.KeepEmailPrivate || authed) { | 	if signed && (!user.KeepEmailPrivate || authed) { | ||||||
| 		result.Email = user.Email | 		result.Email = user.Email | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// only site admin will get these information and possibly user himself | 	// only site admin will get these information and possibly user himself | ||||||
| 	if authed { | 	if authed { | ||||||
| 		result.IsAdmin = user.IsAdmin | 		result.IsAdmin = user.IsAdmin | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
|  | 	api "code.gitea.io/gitea/modules/structs" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| ) | ) | ||||||
| @@ -27,4 +28,11 @@ func TestUser_ToUser(t *testing.T) { | |||||||
|  |  | ||||||
| 	apiUser = toUser(user1, false, false) | 	apiUser = toUser(user1, false, false) | ||||||
| 	assert.False(t, apiUser.IsAdmin) | 	assert.False(t, apiUser.IsAdmin) | ||||||
|  | 	assert.EqualValues(t, api.VisibleTypePublic.String(), apiUser.Visibility) | ||||||
|  |  | ||||||
|  | 	user31 := models.AssertExistsAndLoadBean(t, &models.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate}).(*models.User) | ||||||
|  |  | ||||||
|  | 	apiUser = toUser(user31, true, true) | ||||||
|  | 	assert.False(t, apiUser.IsAdmin) | ||||||
|  | 	assert.EqualValues(t, api.VisibleTypePrivate.String(), apiUser.Visibility) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,6 +15,8 @@ import ( | |||||||
|  |  | ||||||
| // Service settings | // Service settings | ||||||
| var Service struct { | var Service struct { | ||||||
|  | 	DefaultUserVisibility                   string | ||||||
|  | 	DefaultUserVisibilityMode               structs.VisibleType | ||||||
| 	DefaultOrgVisibility                    string | 	DefaultOrgVisibility                    string | ||||||
| 	DefaultOrgVisibilityMode                structs.VisibleType | 	DefaultOrgVisibilityMode                structs.VisibleType | ||||||
| 	ActiveCodeLives                         int | 	ActiveCodeLives                         int | ||||||
| @@ -118,6 +120,8 @@ func newService() { | |||||||
| 	Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true) | 	Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true) | ||||||
| 	Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true) | 	Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true) | ||||||
| 	Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false) | 	Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false) | ||||||
|  | 	Service.DefaultUserVisibility = sec.Key("DEFAULT_USER_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) | ||||||
|  | 	Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility] | ||||||
| 	Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) | 	Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) | ||||||
| 	Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] | 	Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] | ||||||
| 	Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() | 	Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ type CreateUserOption struct { | |||||||
| 	Password           string `json:"password" binding:"Required;MaxSize(255)"` | 	Password           string `json:"password" binding:"Required;MaxSize(255)"` | ||||||
| 	MustChangePassword *bool  `json:"must_change_password"` | 	MustChangePassword *bool  `json:"must_change_password"` | ||||||
| 	SendNotify         bool   `json:"send_notify"` | 	SendNotify         bool   `json:"send_notify"` | ||||||
|  | 	Visibility         string `json:"visibility" binding:"In(,public,limited,private)"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // EditUserOption edit user options | // EditUserOption edit user options | ||||||
| @@ -43,4 +44,5 @@ type EditUserOption struct { | |||||||
| 	ProhibitLogin           *bool   `json:"prohibit_login"` | 	ProhibitLogin           *bool   `json:"prohibit_login"` | ||||||
| 	AllowCreateOrganization *bool   `json:"allow_create_organization"` | 	AllowCreateOrganization *bool   `json:"allow_create_organization"` | ||||||
| 	Restricted              *bool   `json:"restricted"` | 	Restricted              *bool   `json:"restricted"` | ||||||
|  | 	Visibility              string  `json:"visibility" binding:"In(,public,limited,private)"` | ||||||
| } | } | ||||||
|   | |||||||
| @@ -43,6 +43,8 @@ type User struct { | |||||||
| 	Website string `json:"website"` | 	Website string `json:"website"` | ||||||
| 	// the user's description | 	// the user's description | ||||||
| 	Description string `json:"description"` | 	Description string `json:"description"` | ||||||
|  | 	// User visibility level option: public, limited, private | ||||||
|  | 	Visibility string `json:"visibility"` | ||||||
|  |  | ||||||
| 	// user counts | 	// user counts | ||||||
| 	Followers    int `json:"followers_count"` | 	Followers    int `json:"followers_count"` | ||||||
|   | |||||||
| @@ -724,6 +724,14 @@ email_notifications.onmention = Only Email on Mention | |||||||
| email_notifications.disable = Disable Email Notifications | email_notifications.disable = Disable Email Notifications | ||||||
| email_notifications.submit = Set Email Preference | email_notifications.submit = Set Email Preference | ||||||
|  |  | ||||||
|  | visibility = User visibility | ||||||
|  | visibility.public = Public | ||||||
|  | visibility.public_tooltip = Visible to all users | ||||||
|  | visibility.limited = Limited | ||||||
|  | visibility.limited_tooltip = Visible to logged in users only | ||||||
|  | visibility.private = Private | ||||||
|  | visibility.private_tooltip = Visible only to organization members | ||||||
|  |  | ||||||
| [repo] | [repo] | ||||||
| new_repo_helper = A repository contains all project files, including revision history.  Already have it elsewhere? <a href="%s">Migrate repository.</a> | new_repo_helper = A repository contains all project files, including revision history.  Already have it elsewhere? <a href="%s">Migrate repository.</a> | ||||||
| owner = Owner | owner = Owner | ||||||
|   | |||||||
| @@ -66,6 +66,7 @@ func CreateUser(ctx *context.APIContext) { | |||||||
| 	//   "422": | 	//   "422": | ||||||
| 	//     "$ref": "#/responses/validationError" | 	//     "$ref": "#/responses/validationError" | ||||||
| 	form := web.GetForm(ctx).(*api.CreateUserOption) | 	form := web.GetForm(ctx).(*api.CreateUserOption) | ||||||
|  |  | ||||||
| 	u := &models.User{ | 	u := &models.User{ | ||||||
| 		Name:               form.Username, | 		Name:               form.Username, | ||||||
| 		FullName:           form.FullName, | 		FullName:           form.FullName, | ||||||
| @@ -97,7 +98,15 @@ func CreateUser(ctx *context.APIContext) { | |||||||
| 		ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned")) | 		ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned")) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if err := models.CreateUser(u); err != nil { |  | ||||||
|  | 	var overwriteDefault *models.CreateUserOverwriteOptions | ||||||
|  | 	if form.Visibility != "" { | ||||||
|  | 		overwriteDefault = &models.CreateUserOverwriteOptions{ | ||||||
|  | 			Visibility: api.VisibilityModes[form.Visibility], | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := models.CreateUser(u, overwriteDefault); err != nil { | ||||||
| 		if models.IsErrUserAlreadyExist(err) || | 		if models.IsErrUserAlreadyExist(err) || | ||||||
| 			models.IsErrEmailAlreadyUsed(err) || | 			models.IsErrEmailAlreadyUsed(err) || | ||||||
| 			models.IsErrNameReserved(err) || | 			models.IsErrNameReserved(err) || | ||||||
| @@ -209,6 +218,9 @@ func EditUser(ctx *context.APIContext) { | |||||||
| 	if form.Active != nil { | 	if form.Active != nil { | ||||||
| 		u.IsActive = *form.Active | 		u.IsActive = *form.Active | ||||||
| 	} | 	} | ||||||
|  | 	if len(form.Visibility) != 0 { | ||||||
|  | 		u.Visibility = api.VisibilityModes[form.Visibility] | ||||||
|  | 	} | ||||||
| 	if form.Admin != nil { | 	if form.Admin != nil { | ||||||
| 		u.IsAdmin = *form.Admin | 		u.IsAdmin = *form.Admin | ||||||
| 	} | 	} | ||||||
| @@ -395,6 +407,7 @@ func GetAllUsers(ctx *context.APIContext) { | |||||||
| 	listOptions := utils.GetListOptions(ctx) | 	listOptions := utils.GetListOptions(ctx) | ||||||
|  |  | ||||||
| 	users, maxResults, err := models.SearchUsers(&models.SearchUserOptions{ | 	users, maxResults, err := models.SearchUsers(&models.SearchUserOptions{ | ||||||
|  | 		Actor:       ctx.User, | ||||||
| 		Type:        models.UserTypeIndividual, | 		Type:        models.UserTypeIndividual, | ||||||
| 		OrderBy:     models.SearchOrderByAlphabetically, | 		OrderBy:     models.SearchOrderByAlphabetically, | ||||||
| 		ListOptions: listOptions, | 		ListOptions: listOptions, | ||||||
|   | |||||||
| @@ -225,8 +225,8 @@ func Get(ctx *context.APIContext) { | |||||||
| 	//   "200": | 	//   "200": | ||||||
| 	//     "$ref": "#/responses/Organization" | 	//     "$ref": "#/responses/Organization" | ||||||
|  |  | ||||||
| 	if !models.HasOrgVisible(ctx.Org.Organization, ctx.User) { | 	if !models.HasOrgOrUserVisible(ctx.Org.Organization, ctx.User) { | ||||||
| 		ctx.NotFound("HasOrgVisible", nil) | 		ctx.NotFound("HasOrgOrUserVisible", nil) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	ctx.JSON(http.StatusOK, convert.ToOrganization(ctx.Org.Organization)) | 	ctx.JSON(http.StatusOK, convert.ToOrganization(ctx.Org.Organization)) | ||||||
|   | |||||||
| @@ -375,8 +375,8 @@ func CreateOrgRepo(ctx *context.APIContext) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if !models.HasOrgVisible(org, ctx.User) { | 	if !models.HasOrgOrUserVisible(org, ctx.User) { | ||||||
| 		ctx.NotFound("HasOrgVisible", nil) | 		ctx.NotFound("HasOrgOrUserVisible", nil) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ func GetUserByParamsName(ctx *context.APIContext, name string) *models.User { | |||||||
| 	user, err := models.GetUserByName(username) | 	user, err := models.GetUserByName(username) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if models.IsErrUserNotExist(err) { | 		if models.IsErrUserNotExist(err) { | ||||||
| 			if redirectUserID, err := models.LookupUserRedirect(username); err == nil { | 			if redirectUserID, err2 := models.LookupUserRedirect(username); err2 == nil { | ||||||
| 				context.RedirectToUser(ctx.Context, username, redirectUserID) | 				context.RedirectToUser(ctx.Context, username, redirectUserID) | ||||||
| 			} else { | 			} else { | ||||||
| 				ctx.NotFound("GetUserByName", err) | 				ctx.NotFound("GetUserByName", err) | ||||||
|   | |||||||
| @@ -57,6 +57,7 @@ func Search(ctx *context.APIContext) { | |||||||
| 	listOptions := utils.GetListOptions(ctx) | 	listOptions := utils.GetListOptions(ctx) | ||||||
|  |  | ||||||
| 	opts := &models.SearchUserOptions{ | 	opts := &models.SearchUserOptions{ | ||||||
|  | 		Actor:       ctx.User, | ||||||
| 		Keyword:     strings.Trim(ctx.Query("q"), " "), | 		Keyword:     strings.Trim(ctx.Query("q"), " "), | ||||||
| 		UID:         ctx.QueryInt64("uid"), | 		UID:         ctx.QueryInt64("uid"), | ||||||
| 		Type:        models.UserTypeIndividual, | 		Type:        models.UserTypeIndividual, | ||||||
| @@ -102,10 +103,16 @@ func GetInfo(ctx *context.APIContext) { | |||||||
| 	//     "$ref": "#/responses/notFound" | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
| 	u := GetUserByParams(ctx) | 	u := GetUserByParams(ctx) | ||||||
|  |  | ||||||
| 	if ctx.Written() { | 	if ctx.Written() { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if !u.IsVisibleToUser(ctx.User) { | ||||||
|  | 		// fake ErrUserNotExist error message to not leak information about existence | ||||||
|  | 		ctx.NotFound("GetUserByName", models.ErrUserNotExist{Name: ctx.Params(":username")}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
| 	ctx.JSON(http.StatusOK, convert.ToUser(u, ctx.User)) | 	ctx.JSON(http.StatusOK, convert.ToUser(u, ctx.User)) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ func Organizations(ctx *context.Context) { | |||||||
| 	ctx.Data["PageIsAdminOrganizations"] = true | 	ctx.Data["PageIsAdminOrganizations"] = true | ||||||
|  |  | ||||||
| 	explore.RenderUserSearch(ctx, &models.SearchUserOptions{ | 	explore.RenderUserSearch(ctx, &models.SearchUserOptions{ | ||||||
|  | 		Actor: ctx.User, | ||||||
| 		Type:  models.UserTypeOrganization, | 		Type:  models.UserTypeOrganization, | ||||||
| 		ListOptions: models.ListOptions{ | 		ListOptions: models.ListOptions{ | ||||||
| 			PageSize: setting.UI.Admin.OrgPagingNum, | 			PageSize: setting.UI.Admin.OrgPagingNum, | ||||||
|   | |||||||
| @@ -37,6 +37,7 @@ func Users(ctx *context.Context) { | |||||||
| 	ctx.Data["PageIsAdminUsers"] = true | 	ctx.Data["PageIsAdminUsers"] = true | ||||||
|  |  | ||||||
| 	explore.RenderUserSearch(ctx, &models.SearchUserOptions{ | 	explore.RenderUserSearch(ctx, &models.SearchUserOptions{ | ||||||
|  | 		Actor: ctx.User, | ||||||
| 		Type:  models.UserTypeIndividual, | 		Type:  models.UserTypeIndividual, | ||||||
| 		ListOptions: models.ListOptions{ | 		ListOptions: models.ListOptions{ | ||||||
| 			PageSize: setting.UI.Admin.UserPagingNum, | 			PageSize: setting.UI.Admin.UserPagingNum, | ||||||
| @@ -50,6 +51,7 @@ func NewUser(ctx *context.Context) { | |||||||
| 	ctx.Data["Title"] = ctx.Tr("admin.users.new_account") | 	ctx.Data["Title"] = ctx.Tr("admin.users.new_account") | ||||||
| 	ctx.Data["PageIsAdmin"] = true | 	ctx.Data["PageIsAdmin"] = true | ||||||
| 	ctx.Data["PageIsAdminUsers"] = true | 	ctx.Data["PageIsAdminUsers"] = true | ||||||
|  | 	ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode | ||||||
|  |  | ||||||
| 	ctx.Data["login_type"] = "0-0" | 	ctx.Data["login_type"] = "0-0" | ||||||
|  |  | ||||||
| @@ -70,6 +72,7 @@ func NewUserPost(ctx *context.Context) { | |||||||
| 	ctx.Data["Title"] = ctx.Tr("admin.users.new_account") | 	ctx.Data["Title"] = ctx.Tr("admin.users.new_account") | ||||||
| 	ctx.Data["PageIsAdmin"] = true | 	ctx.Data["PageIsAdmin"] = true | ||||||
| 	ctx.Data["PageIsAdminUsers"] = true | 	ctx.Data["PageIsAdminUsers"] = true | ||||||
|  | 	ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode | ||||||
|  |  | ||||||
| 	sources, err := models.LoginSources() | 	sources, err := models.LoginSources() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -126,7 +129,8 @@ func NewUserPost(ctx *context.Context) { | |||||||
| 		} | 		} | ||||||
| 		u.MustChangePassword = form.MustChangePassword | 		u.MustChangePassword = form.MustChangePassword | ||||||
| 	} | 	} | ||||||
| 	if err := models.CreateUser(u); err != nil { |  | ||||||
|  | 	if err := models.CreateUser(u, &models.CreateUserOverwriteOptions{Visibility: form.Visibility}); err != nil { | ||||||
| 		switch { | 		switch { | ||||||
| 		case models.IsErrUserAlreadyExist(err): | 		case models.IsErrUserAlreadyExist(err): | ||||||
| 			ctx.Data["Err_UserName"] = true | 			ctx.Data["Err_UserName"] = true | ||||||
| @@ -312,6 +316,8 @@ func EditUserPost(ctx *context.Context) { | |||||||
| 	u.AllowImportLocal = form.AllowImportLocal | 	u.AllowImportLocal = form.AllowImportLocal | ||||||
| 	u.AllowCreateOrganization = form.AllowCreateOrganization | 	u.AllowCreateOrganization = form.AllowCreateOrganization | ||||||
|  |  | ||||||
|  | 	u.Visibility = form.Visibility | ||||||
|  |  | ||||||
| 	// skip self Prohibit Login | 	// skip self Prohibit Login | ||||||
| 	if ctx.User.ID == u.ID { | 	if ctx.User.ID == u.ID { | ||||||
| 		u.ProhibitLogin = false | 		u.ProhibitLogin = false | ||||||
|   | |||||||
| @@ -8,6 +8,8 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/test" | 	"code.gitea.io/gitea/modules/test" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| 	"code.gitea.io/gitea/services/forms" | 	"code.gitea.io/gitea/services/forms" | ||||||
| @@ -121,3 +123,82 @@ func TestNewUserPost_InvalidEmail(t *testing.T) { | |||||||
|  |  | ||||||
| 	assert.NotEmpty(t, ctx.Flash.ErrorMsg) | 	assert.NotEmpty(t, ctx.Flash.ErrorMsg) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestNewUserPost_VisiblityDefaultPublic(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	models.PrepareTestEnv(t) | ||||||
|  | 	ctx := test.MockContext(t, "admin/users/new") | ||||||
|  |  | ||||||
|  | 	u := models.AssertExistsAndLoadBean(t, &models.User{ | ||||||
|  | 		IsAdmin: true, | ||||||
|  | 		ID:      2, | ||||||
|  | 	}).(*models.User) | ||||||
|  |  | ||||||
|  | 	ctx.User = u | ||||||
|  |  | ||||||
|  | 	username := "gitea" | ||||||
|  | 	email := "gitea@gitea.io" | ||||||
|  |  | ||||||
|  | 	form := forms.AdminCreateUserForm{ | ||||||
|  | 		LoginType:          "local", | ||||||
|  | 		LoginName:          "local", | ||||||
|  | 		UserName:           username, | ||||||
|  | 		Email:              email, | ||||||
|  | 		Password:           "abc123ABC!=$", | ||||||
|  | 		SendNotify:         false, | ||||||
|  | 		MustChangePassword: false, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	web.SetForm(ctx, &form) | ||||||
|  | 	NewUserPost(ctx) | ||||||
|  |  | ||||||
|  | 	assert.NotEmpty(t, ctx.Flash.SuccessMsg) | ||||||
|  |  | ||||||
|  | 	u, err := models.GetUserByName(username) | ||||||
|  |  | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, username, u.Name) | ||||||
|  | 	assert.Equal(t, email, u.Email) | ||||||
|  | 	// As default user visibility | ||||||
|  | 	assert.Equal(t, setting.Service.DefaultUserVisibilityMode, u.Visibility) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestNewUserPost_VisibilityPrivate(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	models.PrepareTestEnv(t) | ||||||
|  | 	ctx := test.MockContext(t, "admin/users/new") | ||||||
|  |  | ||||||
|  | 	u := models.AssertExistsAndLoadBean(t, &models.User{ | ||||||
|  | 		IsAdmin: true, | ||||||
|  | 		ID:      2, | ||||||
|  | 	}).(*models.User) | ||||||
|  |  | ||||||
|  | 	ctx.User = u | ||||||
|  |  | ||||||
|  | 	username := "gitea" | ||||||
|  | 	email := "gitea@gitea.io" | ||||||
|  |  | ||||||
|  | 	form := forms.AdminCreateUserForm{ | ||||||
|  | 		LoginType:          "local", | ||||||
|  | 		LoginName:          "local", | ||||||
|  | 		UserName:           username, | ||||||
|  | 		Email:              email, | ||||||
|  | 		Password:           "abc123ABC!=$", | ||||||
|  | 		SendNotify:         false, | ||||||
|  | 		MustChangePassword: false, | ||||||
|  | 		Visibility:         api.VisibleTypePrivate, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	web.SetForm(ctx, &form) | ||||||
|  | 	NewUserPost(ctx) | ||||||
|  |  | ||||||
|  | 	assert.NotEmpty(t, ctx.Flash.SuccessMsg) | ||||||
|  |  | ||||||
|  | 	u, err := models.GetUserByName(username) | ||||||
|  |  | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.Equal(t, username, u.Name) | ||||||
|  | 	assert.Equal(t, email, u.Email) | ||||||
|  | 	// As default user visibility | ||||||
|  | 	assert.True(t, u.Visibility.IsPrivate()) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -30,8 +30,8 @@ func Home(ctx *context.Context) { | |||||||
|  |  | ||||||
| 	org := ctx.Org.Organization | 	org := ctx.Org.Organization | ||||||
|  |  | ||||||
| 	if !models.HasOrgVisible(org, ctx.User) { | 	if !models.HasOrgOrUserVisible(org, ctx.User) { | ||||||
| 		ctx.NotFound("HasOrgVisible", nil) | 		ctx.NotFound("HasOrgOrUserVisible", nil) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -75,6 +75,17 @@ func Profile(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if ctxUser.IsOrganization() { | ||||||
|  | 		org.Home(ctx) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// check view permissions | ||||||
|  | 	if !ctxUser.IsVisibleToUser(ctx.User) { | ||||||
|  | 		ctx.NotFound("user", fmt.Errorf(uname)) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Show SSH keys. | 	// Show SSH keys. | ||||||
| 	if isShowKeys { | 	if isShowKeys { | ||||||
| 		ShowSSHKeys(ctx, ctxUser.ID) | 		ShowSSHKeys(ctx, ctxUser.ID) | ||||||
| @@ -87,11 +98,6 @@ func Profile(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if ctxUser.IsOrganization() { |  | ||||||
| 		org.Home(ctx) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Show OpenID URIs | 	// Show OpenID URIs | ||||||
| 	openIDs, err := models.GetUserOpenIDs(ctxUser.ID) | 	openIDs, err := models.GetUserOpenIDs(ctxUser.ID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -114,6 +114,7 @@ func ProfilePost(ctx *context.Context) { | |||||||
| 	} | 	} | ||||||
| 	ctx.User.Description = form.Description | 	ctx.User.Description = form.Description | ||||||
| 	ctx.User.KeepActivityPrivate = form.KeepActivityPrivate | 	ctx.User.KeepActivityPrivate = form.KeepActivityPrivate | ||||||
|  | 	ctx.User.Visibility = form.Visibility | ||||||
| 	if err := models.UpdateUserSetting(ctx.User); err != nil { | 	if err := models.UpdateUserSetting(ctx.User); err != nil { | ||||||
| 		if _, ok := err.(models.ErrEmailAlreadyUsed); ok { | 		if _, ok := err.(models.ErrEmailAlreadyUsed); ok { | ||||||
| 			ctx.Flash.Error(ctx.Tr("form.email_been_used")) | 			ctx.Flash.Error(ctx.Tr("form.email_been_used")) | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	"code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/web/middleware" | 	"code.gitea.io/gitea/modules/web/middleware" | ||||||
|  |  | ||||||
| 	"gitea.com/go-chi/binding" | 	"gitea.com/go-chi/binding" | ||||||
| @@ -22,6 +23,7 @@ type AdminCreateUserForm struct { | |||||||
| 	Password           string `binding:"MaxSize(255)"` | 	Password           string `binding:"MaxSize(255)"` | ||||||
| 	SendNotify         bool | 	SendNotify         bool | ||||||
| 	MustChangePassword bool | 	MustChangePassword bool | ||||||
|  | 	Visibility         structs.VisibleType | ||||||
| } | } | ||||||
|  |  | ||||||
| // Validate validates form fields | // Validate validates form fields | ||||||
| @@ -49,6 +51,7 @@ type AdminEditUserForm struct { | |||||||
| 	AllowCreateOrganization bool | 	AllowCreateOrganization bool | ||||||
| 	ProhibitLogin           bool | 	ProhibitLogin           bool | ||||||
| 	Reset2FA                bool `form:"reset_2fa"` | 	Reset2FA                bool `form:"reset_2fa"` | ||||||
|  | 	Visibility              structs.VisibleType | ||||||
| } | } | ||||||
|  |  | ||||||
| // Validate validates form fields | // Validate validates form fields | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ import ( | |||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/web/middleware" | 	"code.gitea.io/gitea/modules/web/middleware" | ||||||
|  |  | ||||||
| 	"gitea.com/go-chi/binding" | 	"gitea.com/go-chi/binding" | ||||||
| @@ -230,6 +231,7 @@ type UpdateProfileForm struct { | |||||||
| 	Location            string `binding:"MaxSize(50)"` | 	Location            string `binding:"MaxSize(50)"` | ||||||
| 	Language            string | 	Language            string | ||||||
| 	Description         string `binding:"MaxSize(255)"` | 	Description         string `binding:"MaxSize(255)"` | ||||||
|  | 	Visibility          structs.VisibleType | ||||||
| 	KeepActivityPrivate bool | 	KeepActivityPrivate bool | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -28,6 +28,33 @@ | |||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
|  |  | ||||||
|  | 				<div class="inline field {{if .Err_Visibility}}error{{end}}"> | ||||||
|  | 					<span class="inline required field"><label for="visibility">{{.i18n.Tr "settings.visibility"}}</label></span> | ||||||
|  | 					<div class="ui selection type dropdown"> | ||||||
|  | 						{{if .User.Visibility.IsPublic}} | ||||||
|  | 						<input type="hidden" id="visibility" name="visibility" value="0"> | ||||||
|  | 						{{end}} | ||||||
|  | 						{{if .User.Visibility.IsLimited}} | ||||||
|  | 						<input type="hidden" id="visibility" name="visibility" value="1"> | ||||||
|  | 						{{end}} | ||||||
|  | 						{{if .User.Visibility.IsPrivate}} | ||||||
|  | 						<input type="hidden" id="visibility" name="visibility" value="2"> | ||||||
|  | 						{{end}} | ||||||
|  | 						<div class="text"> | ||||||
|  | 						{{if .User.Visibility.IsPublic}}{{.i18n.Tr "settings.visibility.public"}}{{end}} | ||||||
|  | 						{{if .User.Visibility.IsLimited}}{{.i18n.Tr "settings.visibility.limited"}}{{end}} | ||||||
|  | 						{{if .User.Visibility.IsPrivate}}{{.i18n.Tr "settings.visibility.private"}}{{end}} | ||||||
|  | 						</div> | ||||||
|  | 						{{svg "octicon-triangle-down" 14 "dropdown icon"}} | ||||||
|  | 						<div class="menu"> | ||||||
|  | 							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.public_tooltip"}}" data-value="0">{{.i18n.Tr "settings.visibility.public"}}</div> | ||||||
|  | 							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.limited_tooltip"}}" data-value="1">{{.i18n.Tr "settings.visibility.limited"}}</div> | ||||||
|  | 							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.private_tooltip"}}" data-value="2">{{.i18n.Tr "settings.visibility.private"}}</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  |  | ||||||
| 				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .User.LoginSource 0}}hide{{end}}"> | 				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .User.LoginSource 0}}hide{{end}}"> | ||||||
| 					<label for="login_name">{{.i18n.Tr "admin.users.auth_login_name"}}</label> | 					<label for="login_name">{{.i18n.Tr "admin.users.auth_login_name"}}</label> | ||||||
| 					<input id="login_name" name="login_name" value="{{.User.LoginName}}" autofocus> | 					<input id="login_name" name="login_name" value="{{.User.LoginName}}" autofocus> | ||||||
|   | |||||||
| @@ -24,6 +24,25 @@ | |||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
|  |  | ||||||
|  | 				<div class="inline field {{if .Err_Visibility}}error{{end}}"> | ||||||
|  | 					<span class="inline required field"><label for="visibility">{{.i18n.Tr "settings.visibility"}}</label></span> | ||||||
|  | 					<div class="ui selection type dropdown"> | ||||||
|  | 						<input type="hidden" id="visibility" name="visibility" value="{{.visibility}}"> | ||||||
|  | 						<div class="text"> | ||||||
|  | 						{{if .DefaultUserVisibilityMode.IsPublic}}{{.i18n.Tr "settings.visibility.public"}}{{end}} | ||||||
|  | 						{{if .DefaultUserVisibilityMode.IsLimited}}{{.i18n.Tr "settings.visibility.limited"}}{{end}} | ||||||
|  | 						{{if .DefaultUserVisibilityMode.IsPrivate}}{{.i18n.Tr "settings.visibility.private"}}{{end}} | ||||||
|  | 						</div> | ||||||
|  | 						{{svg "octicon-triangle-down" 14 "dropdown icon"}} | ||||||
|  | 						<div class="menu"> | ||||||
|  | 							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.public_tooltip"}}" data-value="0">{{.i18n.Tr "settings.visibility.public"}}</div> | ||||||
|  | 							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.limited_tooltip"}}" data-value="1">{{.i18n.Tr "settings.visibility.limited"}}</div> | ||||||
|  | 							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.private_tooltip"}}" data-value="2">{{.i18n.Tr "settings.visibility.private"}}</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  |  | ||||||
| 				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .login_type "0-0"}}hide{{end}}"> | 				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .login_type "0-0"}}hide{{end}}"> | ||||||
| 					<label for="login_name">{{.i18n.Tr "admin.users.auth_login_name"}}</label> | 					<label for="login_name">{{.i18n.Tr "admin.users.auth_login_name"}}</label> | ||||||
| 					<input id="login_name" name="login_name" value="{{.login_name}}"> | 					<input id="login_name" name="login_name" value="{{.login_name}}"> | ||||||
|   | |||||||
| @@ -13334,6 +13334,10 @@ | |||||||
|         "username": { |         "username": { | ||||||
|           "type": "string", |           "type": "string", | ||||||
|           "x-go-name": "Username" |           "x-go-name": "Username" | ||||||
|  |         }, | ||||||
|  |         "visibility": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Visibility" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" |       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||||
| @@ -14143,6 +14147,10 @@ | |||||||
|           "format": "int64", |           "format": "int64", | ||||||
|           "x-go-name": "SourceID" |           "x-go-name": "SourceID" | ||||||
|         }, |         }, | ||||||
|  |         "visibility": { | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Visibility" | ||||||
|  |         }, | ||||||
|         "website": { |         "website": { | ||||||
|           "type": "string", |           "type": "string", | ||||||
|           "x-go-name": "Website" |           "x-go-name": "Website" | ||||||
| @@ -16637,6 +16645,11 @@ | |||||||
|           "format": "int64", |           "format": "int64", | ||||||
|           "x-go-name": "StarredRepos" |           "x-go-name": "StarredRepos" | ||||||
|         }, |         }, | ||||||
|  |         "visibility": { | ||||||
|  |           "description": "User visibility level option: public, limited, private", | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Visibility" | ||||||
|  |         }, | ||||||
|         "website": { |         "website": { | ||||||
|           "description": "the user's website", |           "description": "the user's website", | ||||||
|           "type": "string", |           "type": "string", | ||||||
|   | |||||||
| @@ -61,13 +61,48 @@ | |||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
|  |  | ||||||
|  | 				<div class="ui divider"></div> | ||||||
|  | 				<!-- private block --> | ||||||
|  |  | ||||||
|  | 				<div class="field"> | ||||||
|  | 					<label for="security-private"><strong>{{.i18n.Tr "settings.privacy"}}</strong></label> | ||||||
|  | 				</div> | ||||||
|  |  | ||||||
|  | 				<div class="inline field {{if .Err_Visibility}}error{{end}}"> | ||||||
|  | 					<span class="inline required field"><label for="visibility">{{.i18n.Tr "settings.visibility"}}</label></span> | ||||||
|  | 					<div class="ui selection type dropdown"> | ||||||
|  | 						{{if .SignedUser.Visibility.IsPublic}} | ||||||
|  | 						<input type="hidden" id="visibility" name="visibility" value="0"> | ||||||
|  | 						{{end}} | ||||||
|  | 						{{if .SignedUser.Visibility.IsLimited}} | ||||||
|  | 						<input type="hidden" id="visibility" name="visibility" value="1"> | ||||||
|  | 						{{end}} | ||||||
|  | 						{{if .SignedUser.Visibility.IsPrivate}} | ||||||
|  | 						<input type="hidden" id="visibility" name="visibility" value="2"> | ||||||
|  | 						{{end}} | ||||||
|  | 						<div class="text"> | ||||||
|  | 						{{if .SignedUser.Visibility.IsPublic}}{{.i18n.Tr "settings.visibility.public"}}{{end}} | ||||||
|  | 						{{if .SignedUser.Visibility.IsLimited}}{{.i18n.Tr "settings.visibility.limited"}}{{end}} | ||||||
|  | 						{{if .SignedUser.Visibility.IsPrivate}}{{.i18n.Tr "settings.visibility.private"}}{{end}} | ||||||
|  | 						</div> | ||||||
|  | 						{{svg "octicon-triangle-down" 14 "dropdown icon"}} | ||||||
|  | 						<div class="menu"> | ||||||
|  | 							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.public_tooltip"}}" data-value="0">{{.i18n.Tr "settings.visibility.public"}}</div> | ||||||
|  | 							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.limited_tooltip"}}" data-value="1">{{.i18n.Tr "settings.visibility.limited"}}</div> | ||||||
|  | 							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.private_tooltip"}}" data-value="2">{{.i18n.Tr "settings.visibility.private"}}</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  |  | ||||||
| 				<div class="field"> | 				<div class="field"> | ||||||
| 					<label for="keep-activity-private">{{.i18n.Tr "settings.privacy"}}</label> |  | ||||||
| 					<div class="ui checkbox" id="keep-activity-private"> | 					<div class="ui checkbox" id="keep-activity-private"> | ||||||
| 						<label class="poping up" data-content="{{.i18n.Tr "settings.keep_activity_private_popup"}}"><strong>{{.i18n.Tr "settings.keep_activity_private"}}</strong></label> | 						<label class="poping up" data-content="{{.i18n.Tr "settings.keep_activity_private_popup"}}"><strong>{{.i18n.Tr "settings.keep_activity_private"}}</strong></label> | ||||||
| 						<input name="keep_activity_private" type="checkbox" {{if .SignedUser.KeepActivityPrivate}}checked{{end}}> | 						<input name="keep_activity_private" type="checkbox" {{if .SignedUser.KeepActivityPrivate}}checked{{end}}> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
|  |  | ||||||
|  | 				<div class="ui divider"></div> | ||||||
|  |  | ||||||
| 				<div class="field"> | 				<div class="field"> | ||||||
| 					<button class="ui green button">{{$.i18n.Tr "settings.update_profile"}}</button> | 					<button class="ui green button">{{$.i18n.Tr "settings.update_profile"}}</button> | ||||||
| 				</div> | 				</div> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user