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 | ||||
| ;; | ||||
| ;; Either "public", "limited" or "private", default is "public" | ||||
| ;; Limited is for signed user only | ||||
| ;; Private is only for member of the organization | ||||
| ;; Public is for everyone | ||||
| ;; Limited is for users visible only to signed users | ||||
| ;; Private is for users visible only to members of their organizations | ||||
| ;; 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 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 | ||||
| - `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 | ||||
| - `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_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. | ||||
|   | ||||
| @@ -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 | ||||
|   is_active: true | ||||
|  | ||||
|  | ||||
| - | ||||
|   id: 30 | ||||
|   lower_name: user30 | ||||
| @@ -525,3 +524,20 @@ | ||||
|   avatar_email: user30@example.com | ||||
|   num_repos: 2 | ||||
|   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) | ||||
| } | ||||
|  | ||||
| // HasOrgVisible tells if the given user can see the given org | ||||
| func HasOrgVisible(org, user *User) bool { | ||||
| 	return hasOrgVisible(x, org, user) | ||||
| // HasOrgOrUserVisible tells if the given user can see the given org or user | ||||
| func HasOrgOrUserVisible(org, user *User) bool { | ||||
| 	return hasOrgOrUserVisible(x, org, user) | ||||
| } | ||||
|  | ||||
| func hasOrgVisible(e Engine, org, user *User) bool { | ||||
| func hasOrgOrUserVisible(e Engine, orgOrUser, user *User) bool { | ||||
| 	// Not SignedUser | ||||
| 	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 | ||||
| 	} | ||||
|  | ||||
| 	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 true | ||||
| @@ -483,7 +483,7 @@ func HasOrgsVisible(orgs []*User, user *User) bool { | ||||
| 	} | ||||
|  | ||||
| 	for _, org := range orgs { | ||||
| 		if HasOrgVisible(org, user) { | ||||
| 		if HasOrgOrUserVisible(org, user) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -586,9 +586,9 @@ func TestHasOrgVisibleTypePublic(t *testing.T) { | ||||
| 	assert.NoError(t, CreateOrganization(org, owner)) | ||||
| 	org = AssertExistsAndLoadBean(t, | ||||
| 		&User{Name: org.Name, Type: UserTypeOrganization}).(*User) | ||||
| 	test1 := HasOrgVisible(org, owner) | ||||
| 	test2 := HasOrgVisible(org, user3) | ||||
| 	test3 := HasOrgVisible(org, nil) | ||||
| 	test1 := HasOrgOrUserVisible(org, owner) | ||||
| 	test2 := HasOrgOrUserVisible(org, user3) | ||||
| 	test3 := HasOrgOrUserVisible(org, nil) | ||||
| 	assert.True(t, test1) // owner of org | ||||
| 	assert.True(t, test2) // user not a part of org | ||||
| 	assert.True(t, test3) // logged out user | ||||
| @@ -609,9 +609,9 @@ func TestHasOrgVisibleTypeLimited(t *testing.T) { | ||||
| 	assert.NoError(t, CreateOrganization(org, owner)) | ||||
| 	org = AssertExistsAndLoadBean(t, | ||||
| 		&User{Name: org.Name, Type: UserTypeOrganization}).(*User) | ||||
| 	test1 := HasOrgVisible(org, owner) | ||||
| 	test2 := HasOrgVisible(org, user3) | ||||
| 	test3 := HasOrgVisible(org, nil) | ||||
| 	test1 := HasOrgOrUserVisible(org, owner) | ||||
| 	test2 := HasOrgOrUserVisible(org, user3) | ||||
| 	test3 := HasOrgOrUserVisible(org, nil) | ||||
| 	assert.True(t, test1)  // owner of org | ||||
| 	assert.True(t, test2)  // user not a part of org | ||||
| 	assert.False(t, test3) // logged out user | ||||
| @@ -632,9 +632,9 @@ func TestHasOrgVisibleTypePrivate(t *testing.T) { | ||||
| 	assert.NoError(t, CreateOrganization(org, owner)) | ||||
| 	org = AssertExistsAndLoadBean(t, | ||||
| 		&User{Name: org.Name, Type: UserTypeOrganization}).(*User) | ||||
| 	test1 := HasOrgVisible(org, owner) | ||||
| 	test2 := HasOrgVisible(org, user3) | ||||
| 	test3 := HasOrgVisible(org, nil) | ||||
| 	test1 := HasOrgOrUserVisible(org, owner) | ||||
| 	test2 := HasOrgOrUserVisible(org, user3) | ||||
| 	test3 := HasOrgOrUserVisible(org, nil) | ||||
| 	assert.True(t, test1)  // owner of org | ||||
| 	assert.False(t, test2) // user not a part of org | ||||
| 	assert.False(t, test3) // logged out user | ||||
|   | ||||
| @@ -585,8 +585,7 @@ func (repo *Repository) getReviewers(e Engine, doerID, posterID int64) ([]*User, | ||||
|  | ||||
| 	var users []*User | ||||
|  | ||||
| 	if repo.IsPrivate || | ||||
| 		(repo.Owner.IsOrganization() && repo.Owner.Visibility == api.VisibleTypePrivate) { | ||||
| 	if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate { | ||||
| 		// This a private repository: | ||||
| 		// Anyone who can read the repository is a requestable reviewer | ||||
| 		if err := e. | ||||
|   | ||||
| @@ -176,9 +176,9 @@ func getUserRepoPermission(e Engine, repo *Repository, user *User) (perm Permiss | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Prevent strangers from checking out public repo of private orginization | ||||
| 	// Allow user if they are collaborator of a repo within a private orginization but not a member of the orginization itself | ||||
| 	if repo.Owner.IsOrganization() && !hasOrgVisible(e, repo.Owner, user) && !isCollaborator { | ||||
| 	// Prevent strangers from checking out public repo of private orginization/users | ||||
| 	// 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 !hasOrgOrUserVisible(e, repo.Owner, user) && !isCollaborator { | ||||
| 		perm.AccessMode = AccessModeNone | ||||
| 		return | ||||
| 	} | ||||
|   | ||||
| @@ -432,6 +432,62 @@ 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(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. | ||||
| func (u *User) IsOrganization() bool { | ||||
| 	return u.Type == UserTypeOrganization | ||||
| @@ -796,8 +852,13 @@ func IsUsableUsername(name string) error { | ||||
| 	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. | ||||
| func CreateUser(u *User) (err error) { | ||||
| func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { | ||||
| 	if err = IsUsableUsername(u.Name); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -831,8 +892,6 @@ func CreateUser(u *User) (err error) { | ||||
| 		return ErrEmailAlreadyUsed{u.Email} | ||||
| 	} | ||||
|  | ||||
| 	u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate | ||||
|  | ||||
| 	u.LowerName = strings.ToLower(u.Name) | ||||
| 	u.AvatarEmail = u.Email | ||||
| 	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 { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// set system defaults | ||||
| 	u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate | ||||
| 	u.Visibility = setting.Service.DefaultUserVisibilityMode | ||||
| 	u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation | ||||
| 	u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification | ||||
| 	u.MaxRepoCreation = -1 | ||||
| 	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 { | ||||
| 		return err | ||||
| @@ -1527,10 +1594,9 @@ func (opts *SearchUserOptions) toConds() builder.Cond { | ||||
| 		cond = cond.And(keywordCond) | ||||
| 	} | ||||
|  | ||||
| 	// If visibility filtered | ||||
| 	if len(opts.Visible) > 0 { | ||||
| 		cond = cond.And(builder.In("visibility", opts.Visible)) | ||||
| 	} else { | ||||
| 		cond = cond.And(builder.In("visibility", structs.VisibleTypePublic)) | ||||
| 	} | ||||
|  | ||||
| 	if opts.Actor != nil { | ||||
| @@ -1543,6 +1609,9 @@ func (opts *SearchUserOptions) toConds() builder.Cond { | ||||
| 			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 | ||||
| 			if !opts.Actor.IsRestricted { | ||||
| 				accessCond = builder.Or( | ||||
| @@ -1552,9 +1621,17 @@ func (opts *SearchUserOptions) toConds() builder.Cond { | ||||
| 				// 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}))) | ||||
| 			} | ||||
| 			// Don't forget about self | ||||
| 			accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID}) | ||||
| 			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 { | ||||
| 		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, | ||||
| 		StarredRepos: user.NumStars, | ||||
| 	} | ||||
|  | ||||
| 	result.Visibility = user.Visibility.String() | ||||
|  | ||||
| 	// hide primary email if API caller is anonymous or user keep email private | ||||
| 	if signed && (!user.KeepEmailPrivate || authed) { | ||||
| 		result.Email = user.Email | ||||
| 	} | ||||
|  | ||||
| 	// only site admin will get these information and possibly user himself | ||||
| 	if authed { | ||||
| 		result.IsAdmin = user.IsAdmin | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| @@ -27,4 +28,11 @@ func TestUser_ToUser(t *testing.T) { | ||||
|  | ||||
| 	apiUser = toUser(user1, false, false) | ||||
| 	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 | ||||
| var Service struct { | ||||
| 	DefaultUserVisibility                   string | ||||
| 	DefaultUserVisibilityMode               structs.VisibleType | ||||
| 	DefaultOrgVisibility                    string | ||||
| 	DefaultOrgVisibilityMode                structs.VisibleType | ||||
| 	ActiveCodeLives                         int | ||||
| @@ -118,6 +120,8 @@ func newService() { | ||||
| 	Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true) | ||||
| 	Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true) | ||||
| 	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.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] | ||||
| 	Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() | ||||
|   | ||||
| @@ -19,6 +19,7 @@ type CreateUserOption struct { | ||||
| 	Password           string `json:"password" binding:"Required;MaxSize(255)"` | ||||
| 	MustChangePassword *bool  `json:"must_change_password"` | ||||
| 	SendNotify         bool   `json:"send_notify"` | ||||
| 	Visibility         string `json:"visibility" binding:"In(,public,limited,private)"` | ||||
| } | ||||
|  | ||||
| // EditUserOption edit user options | ||||
| @@ -43,4 +44,5 @@ type EditUserOption struct { | ||||
| 	ProhibitLogin           *bool   `json:"prohibit_login"` | ||||
| 	AllowCreateOrganization *bool   `json:"allow_create_organization"` | ||||
| 	Restricted              *bool   `json:"restricted"` | ||||
| 	Visibility              string  `json:"visibility" binding:"In(,public,limited,private)"` | ||||
| } | ||||
|   | ||||
| @@ -43,6 +43,8 @@ type User struct { | ||||
| 	Website string `json:"website"` | ||||
| 	// the user's description | ||||
| 	Description string `json:"description"` | ||||
| 	// User visibility level option: public, limited, private | ||||
| 	Visibility string `json:"visibility"` | ||||
|  | ||||
| 	// user counts | ||||
| 	Followers    int `json:"followers_count"` | ||||
|   | ||||
| @@ -724,6 +724,14 @@ email_notifications.onmention = Only Email on Mention | ||||
| email_notifications.disable = Disable Email Notifications | ||||
| 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] | ||||
| new_repo_helper = A repository contains all project files, including revision history.  Already have it elsewhere? <a href="%s">Migrate repository.</a> | ||||
| owner = Owner | ||||
|   | ||||
| @@ -66,6 +66,7 @@ func CreateUser(ctx *context.APIContext) { | ||||
| 	//   "422": | ||||
| 	//     "$ref": "#/responses/validationError" | ||||
| 	form := web.GetForm(ctx).(*api.CreateUserOption) | ||||
|  | ||||
| 	u := &models.User{ | ||||
| 		Name:               form.Username, | ||||
| 		FullName:           form.FullName, | ||||
| @@ -97,7 +98,15 @@ func CreateUser(ctx *context.APIContext) { | ||||
| 		ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned")) | ||||
| 		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) || | ||||
| 			models.IsErrEmailAlreadyUsed(err) || | ||||
| 			models.IsErrNameReserved(err) || | ||||
| @@ -209,6 +218,9 @@ func EditUser(ctx *context.APIContext) { | ||||
| 	if form.Active != nil { | ||||
| 		u.IsActive = *form.Active | ||||
| 	} | ||||
| 	if len(form.Visibility) != 0 { | ||||
| 		u.Visibility = api.VisibilityModes[form.Visibility] | ||||
| 	} | ||||
| 	if form.Admin != nil { | ||||
| 		u.IsAdmin = *form.Admin | ||||
| 	} | ||||
| @@ -395,6 +407,7 @@ func GetAllUsers(ctx *context.APIContext) { | ||||
| 	listOptions := utils.GetListOptions(ctx) | ||||
|  | ||||
| 	users, maxResults, err := models.SearchUsers(&models.SearchUserOptions{ | ||||
| 		Actor:       ctx.User, | ||||
| 		Type:        models.UserTypeIndividual, | ||||
| 		OrderBy:     models.SearchOrderByAlphabetically, | ||||
| 		ListOptions: listOptions, | ||||
|   | ||||
| @@ -225,8 +225,8 @@ func Get(ctx *context.APIContext) { | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/Organization" | ||||
|  | ||||
| 	if !models.HasOrgVisible(ctx.Org.Organization, ctx.User) { | ||||
| 		ctx.NotFound("HasOrgVisible", nil) | ||||
| 	if !models.HasOrgOrUserVisible(ctx.Org.Organization, ctx.User) { | ||||
| 		ctx.NotFound("HasOrgOrUserVisible", nil) | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.JSON(http.StatusOK, convert.ToOrganization(ctx.Org.Organization)) | ||||
|   | ||||
| @@ -375,8 +375,8 @@ func CreateOrgRepo(ctx *context.APIContext) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !models.HasOrgVisible(org, ctx.User) { | ||||
| 		ctx.NotFound("HasOrgVisible", nil) | ||||
| 	if !models.HasOrgOrUserVisible(org, ctx.User) { | ||||
| 		ctx.NotFound("HasOrgOrUserVisible", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,7 @@ func GetUserByParamsName(ctx *context.APIContext, name string) *models.User { | ||||
| 	user, err := models.GetUserByName(username) | ||||
| 	if err != nil { | ||||
| 		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) | ||||
| 			} else { | ||||
| 				ctx.NotFound("GetUserByName", err) | ||||
|   | ||||
| @@ -57,6 +57,7 @@ func Search(ctx *context.APIContext) { | ||||
| 	listOptions := utils.GetListOptions(ctx) | ||||
|  | ||||
| 	opts := &models.SearchUserOptions{ | ||||
| 		Actor:       ctx.User, | ||||
| 		Keyword:     strings.Trim(ctx.Query("q"), " "), | ||||
| 		UID:         ctx.QueryInt64("uid"), | ||||
| 		Type:        models.UserTypeIndividual, | ||||
| @@ -102,10 +103,16 @@ func GetInfo(ctx *context.APIContext) { | ||||
| 	//     "$ref": "#/responses/notFound" | ||||
|  | ||||
| 	u := GetUserByParams(ctx) | ||||
|  | ||||
| 	if ctx.Written() { | ||||
| 		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)) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -25,6 +25,7 @@ func Organizations(ctx *context.Context) { | ||||
| 	ctx.Data["PageIsAdminOrganizations"] = true | ||||
|  | ||||
| 	explore.RenderUserSearch(ctx, &models.SearchUserOptions{ | ||||
| 		Actor: ctx.User, | ||||
| 		Type:  models.UserTypeOrganization, | ||||
| 		ListOptions: models.ListOptions{ | ||||
| 			PageSize: setting.UI.Admin.OrgPagingNum, | ||||
|   | ||||
| @@ -37,6 +37,7 @@ func Users(ctx *context.Context) { | ||||
| 	ctx.Data["PageIsAdminUsers"] = true | ||||
|  | ||||
| 	explore.RenderUserSearch(ctx, &models.SearchUserOptions{ | ||||
| 		Actor: ctx.User, | ||||
| 		Type:  models.UserTypeIndividual, | ||||
| 		ListOptions: models.ListOptions{ | ||||
| 			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["PageIsAdmin"] = true | ||||
| 	ctx.Data["PageIsAdminUsers"] = true | ||||
| 	ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode | ||||
|  | ||||
| 	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["PageIsAdmin"] = true | ||||
| 	ctx.Data["PageIsAdminUsers"] = true | ||||
| 	ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode | ||||
|  | ||||
| 	sources, err := models.LoginSources() | ||||
| 	if err != nil { | ||||
| @@ -126,7 +129,8 @@ func NewUserPost(ctx *context.Context) { | ||||
| 		} | ||||
| 		u.MustChangePassword = form.MustChangePassword | ||||
| 	} | ||||
| 	if err := models.CreateUser(u); err != nil { | ||||
|  | ||||
| 	if err := models.CreateUser(u, &models.CreateUserOverwriteOptions{Visibility: form.Visibility}); err != nil { | ||||
| 		switch { | ||||
| 		case models.IsErrUserAlreadyExist(err): | ||||
| 			ctx.Data["Err_UserName"] = true | ||||
| @@ -312,6 +316,8 @@ func EditUserPost(ctx *context.Context) { | ||||
| 	u.AllowImportLocal = form.AllowImportLocal | ||||
| 	u.AllowCreateOrganization = form.AllowCreateOrganization | ||||
|  | ||||
| 	u.Visibility = form.Visibility | ||||
|  | ||||
| 	// skip self Prohibit Login | ||||
| 	if ctx.User.ID == u.ID { | ||||
| 		u.ProhibitLogin = false | ||||
|   | ||||
| @@ -8,6 +8,8 @@ import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"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/web" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| @@ -121,3 +123,82 @@ func TestNewUserPost_InvalidEmail(t *testing.T) { | ||||
|  | ||||
| 	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 | ||||
|  | ||||
| 	if !models.HasOrgVisible(org, ctx.User) { | ||||
| 		ctx.NotFound("HasOrgVisible", nil) | ||||
| 	if !models.HasOrgOrUserVisible(org, ctx.User) { | ||||
| 		ctx.NotFound("HasOrgOrUserVisible", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -75,6 +75,17 @@ func Profile(ctx *context.Context) { | ||||
| 		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. | ||||
| 	if isShowKeys { | ||||
| 		ShowSSHKeys(ctx, ctxUser.ID) | ||||
| @@ -87,11 +98,6 @@ func Profile(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if ctxUser.IsOrganization() { | ||||
| 		org.Home(ctx) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Show OpenID URIs | ||||
| 	openIDs, err := models.GetUserOpenIDs(ctxUser.ID) | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -114,6 +114,7 @@ func ProfilePost(ctx *context.Context) { | ||||
| 	} | ||||
| 	ctx.User.Description = form.Description | ||||
| 	ctx.User.KeepActivityPrivate = form.KeepActivityPrivate | ||||
| 	ctx.User.Visibility = form.Visibility | ||||
| 	if err := models.UpdateUserSetting(ctx.User); err != nil { | ||||
| 		if _, ok := err.(models.ErrEmailAlreadyUsed); ok { | ||||
| 			ctx.Flash.Error(ctx.Tr("form.email_been_used")) | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import ( | ||||
| 	"net/http" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/web/middleware" | ||||
|  | ||||
| 	"gitea.com/go-chi/binding" | ||||
| @@ -22,6 +23,7 @@ type AdminCreateUserForm struct { | ||||
| 	Password           string `binding:"MaxSize(255)"` | ||||
| 	SendNotify         bool | ||||
| 	MustChangePassword bool | ||||
| 	Visibility         structs.VisibleType | ||||
| } | ||||
|  | ||||
| // Validate validates form fields | ||||
| @@ -49,6 +51,7 @@ type AdminEditUserForm struct { | ||||
| 	AllowCreateOrganization bool | ||||
| 	ProhibitLogin           bool | ||||
| 	Reset2FA                bool `form:"reset_2fa"` | ||||
| 	Visibility              structs.VisibleType | ||||
| } | ||||
|  | ||||
| // Validate validates form fields | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import ( | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/web/middleware" | ||||
|  | ||||
| 	"gitea.com/go-chi/binding" | ||||
| @@ -230,6 +231,7 @@ type UpdateProfileForm struct { | ||||
| 	Location            string `binding:"MaxSize(50)"` | ||||
| 	Language            string | ||||
| 	Description         string `binding:"MaxSize(255)"` | ||||
| 	Visibility          structs.VisibleType | ||||
| 	KeepActivityPrivate bool | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -28,6 +28,33 @@ | ||||
| 						</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}}"> | ||||
| 					<label for="login_name">{{.i18n.Tr "admin.users.auth_login_name"}}</label> | ||||
| 					<input id="login_name" name="login_name" value="{{.User.LoginName}}" autofocus> | ||||
|   | ||||
| @@ -24,6 +24,25 @@ | ||||
| 						</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}}"> | ||||
| 					<label for="login_name">{{.i18n.Tr "admin.users.auth_login_name"}}</label> | ||||
| 					<input id="login_name" name="login_name" value="{{.login_name}}"> | ||||
|   | ||||
| @@ -13334,6 +13334,10 @@ | ||||
|         "username": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "Username" | ||||
|         }, | ||||
|         "visibility": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "Visibility" | ||||
|         } | ||||
|       }, | ||||
|       "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
| @@ -14143,6 +14147,10 @@ | ||||
|           "format": "int64", | ||||
|           "x-go-name": "SourceID" | ||||
|         }, | ||||
|         "visibility": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "Visibility" | ||||
|         }, | ||||
|         "website": { | ||||
|           "type": "string", | ||||
|           "x-go-name": "Website" | ||||
| @@ -16637,6 +16645,11 @@ | ||||
|           "format": "int64", | ||||
|           "x-go-name": "StarredRepos" | ||||
|         }, | ||||
|         "visibility": { | ||||
|           "description": "User visibility level option: public, limited, private", | ||||
|           "type": "string", | ||||
|           "x-go-name": "Visibility" | ||||
|         }, | ||||
|         "website": { | ||||
|           "description": "the user's website", | ||||
|           "type": "string", | ||||
|   | ||||
| @@ -61,13 +61,48 @@ | ||||
| 					</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"> | ||||
| 					<label for="keep-activity-private">{{.i18n.Tr "settings.privacy"}}</label> | ||||
| 					<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> | ||||
| 						<input name="keep_activity_private" type="checkbox" {{if .SignedUser.KeepActivityPrivate}}checked{{end}}> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | ||||
| 				<div class="ui divider"></div> | ||||
|  | ||||
| 				<div class="field"> | ||||
| 					<button class="ui green button">{{$.i18n.Tr "settings.update_profile"}}</button> | ||||
| 				</div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user