mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	User details page (#26713)
This PR implements a proposal to clean up the admin users table by
moving some information out to a separate user details page (which also
displays some additional information).
Other changes:
- move edit user page from `/admin/users/{id}` to
`/admin/users/{id}/edit` -> `/admin/users/{id}` now shows the user
details page
- show if user is instance administrator as a label instead of a
separate column
- separate explore users template into a page- and a shared one, to make
it possible to use it on the user details page
- fix issue where there was no margin between alert message and
following content on admin pages
<details>
<summary>Screenshots</summary>


</details>
Partially resolves #25939
---------
Co-authored-by: Giteabot <teabot@gitea.io>
			
			
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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"> | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
							
								
								
									
										48
									
								
								templates/admin/user/view.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								templates/admin/user/view.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -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" .}} | ||||
							
								
								
									
										65
									
								
								templates/admin/user/view_details.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								templates/admin/user/view_details.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -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> | ||||
							
								
								
									
										19
									
								
								templates/admin/user/view_emails.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								templates/admin/user/view_emails.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -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> | ||||
							
								
								
									
										31
									
								
								templates/explore/user_list.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								templates/explore/user_list.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -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> | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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, | ||||
| 	}) | ||||
|   | ||||
| @@ -42,3 +42,10 @@ | ||||
| .admin .table th { | ||||
|   white-space: nowrap; | ||||
| } | ||||
|  | ||||
| .admin-responsive-columns { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   gap: 1rem; | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user