mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-11 09:15:31 +02:00
fix(session): address review feedback
- Use shared flex-divided-list/items-with-main classes in session templates so they pick up consistent styling instead of relying on non-existent flex-list/flex-item-* classes. - Add CountUserSessionsByUserID helper and use it in admin user view so we don't materialize the full session list just to compute total/active counts. - Bound the VirtualSessionProvider tombstone map by self-expiring entries via time.AfterFunc, so map size no longer depends on the session GC interval. Co-Authored-By: GitHub Copilot (Claude Opus 4.7) <copilot@github.com>
This commit is contained in:
parent
093061d3ba
commit
3e2fe1fed4
@ -76,6 +76,21 @@ func GetUserSessionsByUserID(ctx context.Context, userID int64) ([]*UserSession,
|
||||
Desc("created_unix").Find(&sessions)
|
||||
}
|
||||
|
||||
// CountUserSessionsByUserID returns (total, active) session counts for a user
|
||||
// without materializing the rows.
|
||||
func CountUserSessionsByUserID(ctx context.Context, userID int64) (total, active int64, err error) {
|
||||
e := db.GetEngine(ctx)
|
||||
total, err = e.Where("user_id = ?", userID).Count(new(UserSession))
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
active, err = e.Where("user_id = ? AND logout_unix = 0", userID).Count(new(UserSession))
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return total, active, nil
|
||||
}
|
||||
|
||||
// InvalidateUserSession marks a session as logged out
|
||||
func InvalidateUserSession(ctx context.Context, sessionID string) error {
|
||||
_, err := db.GetEngine(ctx).Where("id = ? AND logout_unix = 0", sessionID).
|
||||
|
||||
@ -17,6 +17,11 @@ import (
|
||||
postgres "gitea.com/go-chi/session/postgres"
|
||||
)
|
||||
|
||||
// tombstoneTTL is how long a destroyed session ID is remembered so that
|
||||
// concurrent requests releasing after destruction cannot recreate the session
|
||||
// or be re-authenticated.
|
||||
const tombstoneTTL = 10 * time.Minute
|
||||
|
||||
// VirtualSessionProvider represents a shadowed session provider implementation.
|
||||
// It wraps a real session provider and adds "tombstone" tracking for destroyed
|
||||
// sessions so that concurrent requests (e.g. EventSource) cannot accidentally
|
||||
@ -30,6 +35,8 @@ type VirtualSessionProvider struct {
|
||||
// a FileStore reference may call Release() and recreate the file.
|
||||
// By tracking destroyed IDs, Read() returns an inert VirtualStore
|
||||
// that prevents re-authentication and avoids recreating the file.
|
||||
// Entries self-expire after tombstoneTTL via time.AfterFunc so the map
|
||||
// stays bounded regardless of session GC interval.
|
||||
destroyedSIDs sync.Map // sid -> time.Time
|
||||
}
|
||||
|
||||
@ -100,6 +107,10 @@ func (o *VirtualSessionProvider) Destroy(sid string) error {
|
||||
o.lock.Lock()
|
||||
defer o.lock.Unlock()
|
||||
o.destroyedSIDs.Store(sid, time.Now())
|
||||
// Self-expire the tombstone so the map stays bounded between session GCs.
|
||||
time.AfterFunc(tombstoneTTL, func() {
|
||||
o.destroyedSIDs.Delete(sid)
|
||||
})
|
||||
return o.provider.Destroy(sid)
|
||||
}
|
||||
|
||||
@ -124,28 +135,13 @@ func (o *VirtualSessionProvider) GC() {
|
||||
|
||||
o.provider.GC()
|
||||
|
||||
// Clean up tombstone entries and re-destroy any files that may have
|
||||
// been recreated by concurrent requests releasing after destruction.
|
||||
cutoff := time.Now().Add(-10 * time.Minute)
|
||||
var stale []string
|
||||
var active []string
|
||||
o.destroyedSIDs.Range(func(key, value any) bool {
|
||||
sid := key.(string)
|
||||
if value.(time.Time).Before(cutoff) {
|
||||
stale = append(stale, sid)
|
||||
} else {
|
||||
active = append(active, sid)
|
||||
}
|
||||
// Re-destroy any sessions that may have been recreated by concurrent
|
||||
// requests releasing after destruction. Tombstones themselves expire
|
||||
// via time.AfterFunc in Destroy() so no manual cleanup is needed here.
|
||||
o.destroyedSIDs.Range(func(key, _ any) bool {
|
||||
_ = o.provider.Destroy(key.(string))
|
||||
return true
|
||||
})
|
||||
for _, sid := range stale {
|
||||
o.destroyedSIDs.Delete(sid)
|
||||
}
|
||||
if len(active) > 0 {
|
||||
for _, sid := range active {
|
||||
_ = o.provider.Destroy(sid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@ -305,19 +305,13 @@ func ViewUser(ctx *context.Context) {
|
||||
ctx.Data["Users"] = orgs // needed to be able to use explore/user_list template
|
||||
ctx.Data["OrgsTotal"] = len(orgs)
|
||||
|
||||
userSessions, err := auth.GetUserSessionsByUserID(ctx, u.ID)
|
||||
sessionsTotal, sessionsActive, err := auth.CountUserSessionsByUserID(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserSessionsByUserID", err)
|
||||
ctx.ServerError("CountUserSessionsByUserID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["SessionsTotal"] = len(userSessions)
|
||||
activeCount := 0
|
||||
for _, s := range userSessions {
|
||||
if s.LogoutUnix == 0 {
|
||||
activeCount++
|
||||
}
|
||||
}
|
||||
ctx.Data["SessionsActive"] = activeCount
|
||||
ctx.Data["SessionsTotal"] = sessionsTotal
|
||||
ctx.Data["SessionsActive"] = sessionsActive
|
||||
|
||||
ctx.HTML(http.StatusOK, tplUserView)
|
||||
}
|
||||
|
||||
@ -14,14 +14,14 @@
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .Sessions}}
|
||||
<div class="flex-list">
|
||||
<div class="flex-divided-list items-with-main">
|
||||
{{range .Sessions}}
|
||||
<div class="flex-item">
|
||||
<div class="flex-item-leading">
|
||||
<div class="item">
|
||||
<div class="item-leading">
|
||||
{{svg "octicon-device-desktop" 32}}
|
||||
</div>
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-item-title">
|
||||
<div class="item-main">
|
||||
<div class="item-title">
|
||||
{{StringUtils.EllipsisString .UserAgent 60}}
|
||||
{{if .LogoutUnix}}
|
||||
<span class="ui grey label">{{ctx.Locale.Tr "settings.sessions.ended"}}</span>
|
||||
@ -29,7 +29,7 @@
|
||||
<span class="ui green label">{{ctx.Locale.Tr "settings.sessions.active"}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex-item-body">
|
||||
<div class="item-body">
|
||||
<span class="flex-text-inline">{{svg "octicon-globe"}} {{ctx.Locale.Tr "settings.sessions.login_ip"}}: {{.LoginIP}}</span>
|
||||
{{if .LastIP}}
|
||||
<span class="flex-text-inline">| {{ctx.Locale.Tr "settings.sessions.last_ip"}}: {{.LastIP}}</span>
|
||||
@ -38,14 +38,14 @@
|
||||
<span class="flex-text-inline">| {{ctx.Locale.Tr "settings.sessions.prev_ip"}}: {{.PrevIP}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex-item-body">
|
||||
<div class="item-body">
|
||||
<span class="flex-text-inline">{{svg "octicon-key"}} {{.LoginMethod}}</span>
|
||||
<span class="flex-text-inline">| {{svg "octicon-calendar"}} {{DateUtils.AbsoluteShort .CreatedUnix}}</span>
|
||||
<span class="flex-text-inline">| {{ctx.Locale.Tr "settings.sessions.last_active"}}: {{DateUtils.TimeSince .LastAccessUnix}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{if not .LogoutUnix}}
|
||||
<div class="flex-item-trailing">
|
||||
<div class="item-trailing">
|
||||
<form action="{{AppSubUrl}}/-/admin/users/{{$.User.ID}}/sessions/revoke" method="post">
|
||||
<input type="hidden" name="session_id" value="{{.ID}}">
|
||||
<button class="ui red tiny button">{{ctx.Locale.Tr "settings.sessions.revoke"}}</button>
|
||||
|
||||
@ -14,14 +14,14 @@
|
||||
<div class="ui attached segment">
|
||||
<p>{{ctx.Locale.Tr "settings.sessions_desc"}}</p>
|
||||
{{if .Sessions}}
|
||||
<div class="flex-list">
|
||||
<div class="flex-divided-list items-with-main">
|
||||
{{range .Sessions}}
|
||||
<div class="flex-item {{if eq .ID $.CurrentSessionID}}tw-bg-green-50{{end}}">
|
||||
<div class="flex-item-leading">
|
||||
<div class="item {{if eq .ID $.CurrentSessionID}}tw-bg-green-50{{end}}">
|
||||
<div class="item-leading">
|
||||
{{svg "octicon-device-desktop" 32}}
|
||||
</div>
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-item-title">
|
||||
<div class="item-main">
|
||||
<div class="item-title">
|
||||
{{StringUtils.EllipsisString .UserAgent 60}}
|
||||
{{if eq .ID $.CurrentSessionID}}
|
||||
<span class="ui green label">{{ctx.Locale.Tr "settings.sessions.current"}}</span>
|
||||
@ -30,7 +30,7 @@
|
||||
<span class="ui grey label">{{ctx.Locale.Tr "settings.sessions.ended"}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex-item-body">
|
||||
<div class="item-body">
|
||||
<span class="flex-text-inline">{{svg "octicon-globe"}} {{ctx.Locale.Tr "settings.sessions.login_ip"}}: {{.LoginIP}}</span>
|
||||
{{if .LastIP}}
|
||||
<span class="flex-text-inline">| {{ctx.Locale.Tr "settings.sessions.last_ip"}}: {{.LastIP}}</span>
|
||||
@ -39,14 +39,14 @@
|
||||
<span class="flex-text-inline">| {{ctx.Locale.Tr "settings.sessions.prev_ip"}}: {{.PrevIP}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex-item-body">
|
||||
<div class="item-body">
|
||||
<span class="flex-text-inline">{{svg "octicon-key"}} {{.LoginMethod}}</span>
|
||||
<span class="flex-text-inline">| {{svg "octicon-calendar"}} {{DateUtils.AbsoluteShort .CreatedUnix}}</span>
|
||||
<span class="flex-text-inline">| {{ctx.Locale.Tr "settings.sessions.last_active"}}: {{DateUtils.TimeSince .LastAccessUnix}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{if and (not .LogoutUnix) (ne .ID $.CurrentSessionID)}}
|
||||
<div class="flex-item-trailing">
|
||||
<div class="item-trailing">
|
||||
<form action="{{AppSubUrl}}/user/settings/security/sessions/revoke" method="post">
|
||||
<input type="hidden" name="session_id" value="{{.ID}}">
|
||||
<button class="ui red tiny button">{{ctx.Locale.Tr "settings.sessions.revoke"}}</button>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user