diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f8e068fe1970..401692388e5d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2823,6 +2823,7 @@ users.list_status_filter.is_prohibit_login = Prohibit Login users.list_status_filter.not_prohibit_login = Allow Login users.list_status_filter.is_2fa_enabled = 2FA Enabled users.list_status_filter.not_2fa_enabled = 2FA Disabled +users.details = User Details emails.email_manage_panel = User Email Management emails.primary = Primary diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index e560a88b4cff..61df49b85ba1 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -13,6 +13,8 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" + org_model "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" system_model "code.gitea.io/gitea/models/system" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/auth/password" @@ -32,6 +34,7 @@ import ( const ( tplUsers base.TplName = "admin/user/list" tplUserNew base.TplName = "admin/user/new" + tplUserView base.TplName = "admin/user/view" tplUserEdit base.TplName = "admin/user/edit" ) @@ -249,6 +252,61 @@ func prepareUserInfo(ctx *context.Context) *user_model.User { return u } +func ViewUser(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("admin.users.details") + ctx.Data["PageIsAdminUsers"] = true + ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation + ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations + ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() + + u := prepareUserInfo(ctx) + if ctx.Written() { + return + } + + repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + ListAll: true, + }, + OwnerID: u.ID, + OrderBy: db.SearchOrderByAlphabetically, + Private: true, + Collaborate: util.OptionalBoolFalse, + }) + if err != nil { + ctx.ServerError("SearchRepository", err) + return + } + + ctx.Data["Repos"] = repos + ctx.Data["ReposTotal"] = int(count) + + emails, err := user_model.GetEmailAddresses(ctx.Doer.ID) + if err != nil { + ctx.ServerError("GetEmailAddresses", err) + return + } + ctx.Data["Emails"] = emails + ctx.Data["EmailsTotal"] = len(emails) + + orgs, err := org_model.FindOrgs(org_model.FindOrgOptions{ + ListOptions: db.ListOptions{ + ListAll: true, + }, + UserID: u.ID, + IncludePrivate: true, + }) + if err != nil { + ctx.ServerError("FindOrgs", err) + return + } + + ctx.Data["Users"] = orgs // needed to be able to use explore/user_list template + ctx.Data["OrgsTotal"] = len(orgs) + + ctx.HTML(http.StatusOK, tplUserView) +} + // EditUser show editing user page func EditUser(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.users.edit_account") diff --git a/routers/web/web.go b/routers/web/web.go index bbab9b37b597..ec6742f6ce76 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -573,7 +573,8 @@ func registerRoutes(m *web.Route) { m.Group("/users", func() { m.Get("", admin.Users) m.Combo("/new").Get(admin.NewUser).Post(web.Bind(forms.AdminCreateUserForm{}), admin.NewUserPost) - m.Combo("/{userid}").Get(admin.EditUser).Post(web.Bind(forms.AdminEditUserForm{}), admin.EditUserPost) + m.Get("/{userid}", admin.ViewUser) + m.Combo("/{userid}/edit").Get(admin.EditUser).Post(web.Bind(forms.AdminEditUserForm{}), admin.EditUserPost) m.Post("/{userid}/delete", admin.DeleteUser) m.Post("/{userid}/avatar", web.Bind(forms.AvatarForm{}), admin.AvatarPost) m.Post("/{userid}/avatar/delete", admin.DeleteAvatar) diff --git a/templates/admin/layout_head.tmpl b/templates/admin/layout_head.tmpl index 64d03d9e5488..0067f336e05c 100644 --- a/templates/admin/layout_head.tmpl +++ b/templates/admin/layout_head.tmpl @@ -1,6 +1,6 @@ {{template "base/head" .ctxData}} <div role="main" aria-label="{{.ctxData.Title}}" class="page-content {{.pageClass}}"> - <div class="ui container"> + <div class="ui container gt-mb-4"> {{template "base/alert" .ctxData}} </div> <div class="ui container flex-container"> diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index 45fed87a4001..b3e0caa16915 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -68,36 +68,35 @@ </th> <th>{{.locale.Tr "email"}}</th> <th>{{.locale.Tr "admin.users.activated"}}</th> - <th>{{.locale.Tr "admin.users.admin"}}</th> <th>{{.locale.Tr "admin.users.restricted"}}</th> <th>{{.locale.Tr "admin.users.2fa"}}</th> - <th>{{.locale.Tr "admin.users.repos"}}</th> <th>{{.locale.Tr "admin.users.created"}}</th> <th data-sortt-asc="lastlogin" data-sortt-desc="reverselastlogin"> {{.locale.Tr "admin.users.last_login"}} {{SortArrow "lastlogin" "reverselastlogin" $.SortType false}} </th> - <th>{{.locale.Tr "admin.users.edit"}}</th> </tr> </thead> <tbody> {{range .Users}} <tr> <td>{{.ID}}</td> - <td><a href="{{.HomeLink}}">{{.Name}}</a></td> + <td> + <a href="{{$.Link}}/{{.ID}}">{{.Name}}</a> + {{if .IsAdmin}} + <span class="ui basic label">{{$.locale.Tr "admin.users.admin"}}</span> + {{end}} + </td> <td class="gt-ellipsis gt-max-width-12rem">{{.Email}}</td> <td>{{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> - <td>{{if .IsAdmin}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> <td>{{if .IsRestricted}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> <td>{{if index $.UsersTwoFaStatus .ID}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> - <td>{{.NumRepos}}</td> <td>{{DateTime "short" .CreatedUnix}}</td> {{if .LastLoginUnix}} <td>{{DateTime "short" .LastLoginUnix}}</td> {{else}} <td><span>{{$.locale.Tr "admin.users.never_login"}}</span></td> {{end}} - <td><a href="{{$.Link}}/{{.ID}}">{{svg "octicon-pencil"}}</a></td> </tr> {{end}} </tbody> diff --git a/templates/admin/user/view.tmpl b/templates/admin/user/view.tmpl new file mode 100644 index 000000000000..fd3017607cdc --- /dev/null +++ b/templates/admin/user/view.tmpl @@ -0,0 +1,48 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin view user")}} + +<div class="admin-setting-content"> + <div class="admin-responsive-columns"> + <div class="gt-f1"> + <h4 class="ui top attached header"> + {{.Title}} + <div class="ui right"> + <a class="ui primary tiny button" href="{{.Link}}/edit">{{ctx.Locale.Tr "admin.users.edit"}}</a> + </div> + </h4> + <div class="ui attached segment"> + {{template "admin/user/view_details" .}} + </div> + </div> + <div class="gt-f1"> + <h4 class="ui top attached header"> + {{ctx.Locale.Tr "admin.emails"}} + <div class="ui right"> + {{.EmailsTotal}} + </div> + </h4> + <div class="ui attached segment"> + {{template "admin/user/view_emails" .}} + </div> + </div> + </div> + <h4 class="ui top attached header"> + {{ctx.Locale.Tr "admin.repositories"}} + <div class="ui right"> + {{.ReposTotal}} + </div> + </h4> + <div class="ui attached segment"> + {{template "explore/repo_list" .}} + </div> + <h4 class="ui top attached header"> + {{ctx.Locale.Tr "settings.organization"}} + <div class="ui right"> + {{.OrgsTotal}} + </div> + </h4> + <div class="ui attached segment"> + {{template "explore/user_list" .}} + </div> +</div> + +{{template "admin/layout_footer" .}} diff --git a/templates/admin/user/view_details.tmpl b/templates/admin/user/view_details.tmpl new file mode 100644 index 000000000000..ceb3b9a05509 --- /dev/null +++ b/templates/admin/user/view_details.tmpl @@ -0,0 +1,65 @@ +<div class="flex-list"> + <div class="flex-item"> + <div class="flex-item-leading"> + {{ctx.AvatarUtils.Avatar .User 48}} + </div> + <div class="flex-item-main"> + <div class="flex-item-title"> + {{template "shared/user/name" .User}} + {{if .User.IsAdmin}} + <span class="ui basic label">{{ctx.Locale.Tr "admin.users.admin"}}</span> + {{end}} + </div> + <div class="flex-item-body"> + <b>{{ctx.Locale.Tr "admin.users.auth_source"}}:</b> + {{if eq .LoginSource.ID 0}} + {{ctx.Locale.Tr "admin.users.local"}} + {{else}} + {{.LoginSource.Name}} + {{end}} + </div> + <div class="flex-item-body"> + <b>{{ctx.Locale.Tr "admin.users.activated"}}:</b> + {{if .User.IsActive}} + {{svg "octicon-check"}} + {{else}} + {{svg "octicon-x"}} + {{end}} + </div> + <div class="flex-item-body"> + <b>{{ctx.Locale.Tr "admin.users.restricted"}}:</b> + {{if .User.IsRestricted}} + {{svg "octicon-check"}} + {{else}} + {{svg "octicon-x"}} + {{end}} + </div> + <div class="flex-item-body"> + <b>{{ctx.Locale.Tr "settings.visibility"}}:</b> + {{if .User.Visibility.IsLimited}}{{ctx.Locale.Tr "settings.visibility.limited"}}{{end}} + {{if .User.Visibility.IsPrivate}}{{ctx.Locale.Tr "settings.visibility.private"}}{{end}} + </div> + <div class="flex-item-body"> + <b>{{ctx.Locale.Tr "admin.users.2fa"}}:</b> + {{if .TwoFactorEnabled}} + <span class="text green">{{svg "octicon-check"}}</span> + {{else}} + {{svg "octicon-x"}} + {{end}} + </div> + {{if .User.Location}} + <div class="flex-item-body"> + <span class="flex-text-inline">{{svg "octicon-location"}}{{.User.Location}}</span> + </div> + {{end}} + {{if .User.Website}} + <div class="flex-item-body"> + <span class="flex-text-inline"> + {{svg "octicon-link"}} + <a target="_blank" href="{{.User.Website}}">{{.User.Website}}</a> + </span> + </div> + {{end}} + </div> + </div> +</div> diff --git a/templates/admin/user/view_emails.tmpl b/templates/admin/user/view_emails.tmpl new file mode 100644 index 000000000000..22ce305a88bc --- /dev/null +++ b/templates/admin/user/view_emails.tmpl @@ -0,0 +1,19 @@ +<div class="flex-list"> + {{range .Emails}} + <div class="flex-item"> + <div class="flex-item-main"> + <div class="flex-text-block"> + {{.Email}} + {{if .IsPrimary}} + <div class="ui primary label">{{ctx.Locale.Tr "settings.primary"}}</div> + {{end}} + {{if .IsActivated}} + <div class="ui green label">{{ctx.Locale.Tr "settings.activated"}}</div> + {{else}} + <div class="ui label">{{ctx.Locale.Tr "settings.requires_activation"}}</div> + {{end}} + </div> + </div> + </div> + {{end}} +</div> diff --git a/templates/explore/user_list.tmpl b/templates/explore/user_list.tmpl new file mode 100644 index 000000000000..cf6a2933b0fe --- /dev/null +++ b/templates/explore/user_list.tmpl @@ -0,0 +1,31 @@ +<div class="flex-list"> + {{range .Users}} + <div class="flex-item flex-item-center"> + <div class="flex-item-leading"> + {{ctx.AvatarUtils.Avatar . 48}} + </div> + <div class="flex-item-main"> + <div class="flex-item-title"> + {{template "shared/user/name" .}} + {{if .Visibility.IsPrivate}} + <span class="ui basic tiny label">{{ctx.Locale.Tr "repo.desc.private"}}</span> + {{end}} + </div> + <div class="flex-item-body"> + {{if .Location}} + <span class="flex-text-inline">{{svg "octicon-location"}}{{.Location}}</span> + {{end}} + {{if and .Email (or (and $.ShowUserEmail $.IsSigned (not .KeepEmailPrivate)) $.PageIsAdminUsers)}} + <span class="flex-text-inline"> + {{svg "octicon-mail"}} + <a href="mailto:{{.Email}}">{{.Email}}</a> + </span> + {{end}} + <span class="flex-text-inline">{{svg "octicon-calendar"}}{{ctx.Locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix) | Safe}}</span> + </div> + </div> + </div> + {{else}} + <div class="flex-item">{{ctx.Locale.Tr "explore.user_no_results"}}</div> + {{end}} +</div> diff --git a/templates/explore/users.tmpl b/templates/explore/users.tmpl index 1280f4add664..7e15ae3d47f4 100644 --- a/templates/explore/users.tmpl +++ b/templates/explore/users.tmpl @@ -4,37 +4,7 @@ <div class="ui container"> {{template "explore/search" .}} - <div class="flex-list"> - {{range .Users}} - <div class="flex-item flex-item-center"> - <div class="flex-item-leading"> - {{ctx.AvatarUtils.Avatar . 48}} - </div> - <div class="flex-item-main"> - <div class="flex-item-title"> - {{template "shared/user/name" .}} - {{if .Visibility.IsPrivate}} - <span class="ui basic tiny label">{{$.locale.Tr "repo.desc.private"}}</span> - {{end}} - </div> - <div class="flex-item-body"> - {{if .Location}} - <span class="flex-text-inline">{{svg "octicon-location"}}{{.Location}}</span> - {{end}} - {{if and $.ShowUserEmail .Email $.IsSigned (not .KeepEmailPrivate)}} - <span class="flex-text-inline"> - {{svg "octicon-mail"}} - <a href="mailto:{{.Email}}" rel="nofollow">{{.Email}}</a> - </span> - {{end}} - <span class="flex-text-inline">{{svg "octicon-calendar"}}{{$.locale.Tr "user.joined_on" (DateTime "short" .CreatedUnix) | Safe}}</span> - </div> - </div> - </div> - {{else}} - <div class="flex-item">{{$.locale.Tr "explore.user_no_results"}}</div> - {{end}} - </div> + {{template "explore/user_list" .}} {{template "base/paginate" .}} </div> diff --git a/tests/integration/admin_user_test.go b/tests/integration/admin_user_test.go index dd6b9ccbbeb8..669060c787d4 100644 --- a/tests/integration/admin_user_test.go +++ b/tests/integration/admin_user_test.go @@ -51,8 +51,8 @@ func testSuccessfullEdit(t *testing.T, formData user_model.User) { func makeRequest(t *testing.T, formData user_model.User, headerCode int) { session := loginUser(t, "user1") - csrf := GetCSRF(t, session, "/admin/users/"+strconv.Itoa(int(formData.ID))) - req := NewRequestWithValues(t, "POST", "/admin/users/"+strconv.Itoa(int(formData.ID)), map[string]string{ + csrf := GetCSRF(t, session, "/admin/users/"+strconv.Itoa(int(formData.ID))+"/edit") + req := NewRequestWithValues(t, "POST", "/admin/users/"+strconv.Itoa(int(formData.ID))+"/edit", map[string]string{ "_csrf": csrf, "user_name": formData.Name, "login_name": formData.LoginName, @@ -72,7 +72,7 @@ func TestAdminDeleteUser(t *testing.T) { session := loginUser(t, "user1") - csrf := GetCSRF(t, session, "/admin/users/8") + csrf := GetCSRF(t, session, "/admin/users/8/edit") req := NewRequestWithValues(t, "POST", "/admin/users/8/delete", map[string]string{ "_csrf": csrf, }) diff --git a/web_src/css/admin.css b/web_src/css/admin.css index fecae5f2bf98..e6866b27a688 100644 --- a/web_src/css/admin.css +++ b/web_src/css/admin.css @@ -42,3 +42,10 @@ .admin .table th { white-space: nowrap; } + +.admin-responsive-columns { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin-bottom: 1rem; +}