mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-28 22:53:54 +03: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:
parent
19ac575d57
commit
22a0636544
32 changed files with 440 additions and 68 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
105
models/user.go
105
models/user.go
|
@ -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,16 +1609,27 @@ func (opts *SearchUserOptions) toConds() builder.Cond {
|
||||||
exprCond = builder.Expr("org_user.org_id = \"user\".id")
|
exprCond = builder.Expr("org_user.org_id = \"user\".id")
|
||||||
}
|
}
|
||||||
|
|
||||||
var accessCond builder.Cond
|
// If Admin - they see all users!
|
||||||
if !opts.Actor.IsRestricted {
|
if !opts.Actor.IsAdmin {
|
||||||
accessCond = builder.Or(
|
// Force visiblity for privacy
|
||||||
builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))),
|
var accessCond builder.Cond
|
||||||
builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
|
if !opts.Actor.IsRestricted {
|
||||||
} else {
|
accessCond = builder.Or(
|
||||||
// restricted users only see orgs they are a member of
|
builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))),
|
||||||
accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID})))
|
builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
|
||||||
|
} else {
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
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 {
|
||||||
|
|
|
@ -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,7 +25,8 @@ func Organizations(ctx *context.Context) {
|
||||||
ctx.Data["PageIsAdminOrganizations"] = true
|
ctx.Data["PageIsAdminOrganizations"] = true
|
||||||
|
|
||||||
explore.RenderUserSearch(ctx, &models.SearchUserOptions{
|
explore.RenderUserSearch(ctx, &models.SearchUserOptions{
|
||||||
Type: models.UserTypeOrganization,
|
Actor: ctx.User,
|
||||||
|
Type: models.UserTypeOrganization,
|
||||||
ListOptions: models.ListOptions{
|
ListOptions: models.ListOptions{
|
||||||
PageSize: setting.UI.Admin.OrgPagingNum,
|
PageSize: setting.UI.Admin.OrgPagingNum,
|
||||||
},
|
},
|
||||||
|
|
|
@ -37,7 +37,8 @@ func Users(ctx *context.Context) {
|
||||||
ctx.Data["PageIsAdminUsers"] = true
|
ctx.Data["PageIsAdminUsers"] = true
|
||||||
|
|
||||||
explore.RenderUserSearch(ctx, &models.SearchUserOptions{
|
explore.RenderUserSearch(ctx, &models.SearchUserOptions{
|
||||||
Type: models.UserTypeIndividual,
|
Actor: ctx.User,
|
||||||
|
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",
|
||||||
|
|
|
@ -47,27 +47,62 @@
|
||||||
<input id="location" name="location" value="{{.SignedUser.Location}}">
|
<input id="location" name="location" value="{{.SignedUser.Location}}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="language">{{.i18n.Tr "settings.language"}}</label>
|
<label for="language">{{.i18n.Tr "settings.language"}}</label>
|
||||||
<div class="ui language selection dropdown" id="language">
|
<div class="ui language selection dropdown" id="language">
|
||||||
<input name="language" type="hidden" value="{{.SignedUser.Language}}">
|
<input name="language" type="hidden" value="{{.SignedUser.Language}}">
|
||||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
<div class="text">{{range .AllLangs}}{{if eq $.SignedUser.Language .Lang}}{{.Name}}{{end}}{{end}}</div>
|
<div class="text">{{range .AllLangs}}{{if eq $.SignedUser.Language .Lang}}{{.Name}}{{end}}{{end}}</div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
{{range .AllLangs}}
|
{{range .AllLangs}}
|
||||||
<div class="item{{if eq $.SignedUser.Language .Lang}} active selected{{end}}" data-value="{{.Lang}}">{{.Name}}</div>
|
<div class="item{{if eq $.SignedUser.Language .Lang}} active selected{{end}}" data-value="{{.Lang}}">{{.Name}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
||||||
|
|
Loading…
Reference in a new issue