Files
gitea/models/auth/user_session.go
T
Eric Lesiuta 1ea114fa67 Cleanup
- Move len sessions to template
- Move time conversion inside CleanupExpiredUserSessions
2026-03-25 00:03:21 -04:00

142 lines
4.9 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"context"
"fmt"
"time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
// ErrUserSessionNotExist is returned when a user session does not exist
type ErrUserSessionNotExist struct {
ID string
}
func (err ErrUserSessionNotExist) Error() string {
return fmt.Sprintf("user session does not exist [id: %s]", err.ID)
}
func (err ErrUserSessionNotExist) Unwrap() error {
return util.ErrNotExist
}
// IsErrUserSessionNotExist checks if an error is ErrUserSessionNotExist
func IsErrUserSessionNotExist(err error) bool {
_, ok := err.(ErrUserSessionNotExist)
return ok
}
// UserSession represents a tracked user session with metadata
type UserSession struct {
ID string `xorm:"pk VARCHAR(64)"`
UserID int64 `xorm:"INDEX NOT NULL"`
LoginIP string `xorm:"VARCHAR(45)"`
LastIP string `xorm:"VARCHAR(45)"`
PrevIP string `xorm:"VARCHAR(45)"`
UserAgent string `xorm:"TEXT"`
LoginMethod string `xorm:"VARCHAR(64)"`
AuthTokenID string `xorm:"VARCHAR(64)"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"`
LastAccessUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL"`
LogoutUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL DEFAULT 0"`
}
func init() {
db.RegisterModel(new(UserSession))
}
// CreateUserSession inserts a new user session record
func CreateUserSession(ctx context.Context, session *UserSession) error {
return db.Insert(ctx, session)
}
// GetUserSessionByID returns a single session by its ID
func GetUserSessionByID(ctx context.Context, id string) (*UserSession, error) {
sess, has, err := db.Get[UserSession](ctx, builder.Eq{"id": id})
if err != nil {
return nil, err
} else if !has {
return nil, ErrUserSessionNotExist{ID: id}
}
return sess, nil
}
// GetUserSessionsByUserID returns all sessions for a user, ordered by creation time descending
func GetUserSessionsByUserID(ctx context.Context, userID int64) ([]*UserSession, error) {
sessions := make([]*UserSession, 0, 8)
return sessions, db.GetEngine(ctx).Where("user_id = ?", userID).
Desc("created_unix").Find(&sessions)
}
// 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).
Cols("logout_unix").
Update(&UserSession{LogoutUnix: timeutil.TimeStampNow()})
return err
}
// InvalidateAllUserSessions marks all active sessions for a user as logged out,
// optionally excluding a specific session
func InvalidateAllUserSessions(ctx context.Context, userID int64, exceptSessionID string) error {
sess := db.GetEngine(ctx).Where("user_id = ? AND logout_unix = 0", userID)
if exceptSessionID != "" {
sess = sess.And("id != ?", exceptSessionID)
}
_, err := sess.Cols("logout_unix").Update(&UserSession{LogoutUnix: timeutil.TimeStampNow()})
return err
}
// UpdateSessionActivity updates the last access time and IP shift logic
// using a single UPDATE statement with no prior SELECT.
// Only updates sessions that are still active (not yet logged out).
func UpdateSessionActivity(ctx context.Context, sessionID, currentIP string) error {
now := int64(timeutil.TimeStampNow())
if currentIP == "" {
_, err := db.GetEngine(ctx).Exec(
"UPDATE user_session SET last_access_unix = ? WHERE id = ? AND logout_unix = 0",
now, sessionID,
)
return err
}
_, err := db.GetEngine(ctx).Exec(
"UPDATE user_session SET last_access_unix = ?,"+
" prev_ip = CASE WHEN last_ip != ? AND last_ip != '' THEN last_ip ELSE prev_ip END,"+
" last_ip = ? WHERE id = ? AND logout_unix = 0",
now, currentIP, currentIP, sessionID,
)
return err
}
// CleanupExpiredUserSessions removes old session records based on retention policy.
// It deletes:
// - Sessions that were logged out more than retention ago
// - Abandoned sessions (never logged out) whose last activity is older than maxLifetime + retention
func CleanupExpiredUserSessions(ctx context.Context, retention, maxLifetime time.Duration) error {
now := int64(timeutil.TimeStampNow())
logoutCutoff := now - int64(retention.Seconds())
abandonedCutoff := now - int64(maxLifetime.Seconds()) - int64(retention.Seconds())
_, err := db.GetEngine(ctx).Where(
builder.Or(
builder.And(builder.Gt{"logout_unix": 0}, builder.Lt{"logout_unix": logoutCutoff}),
builder.And(builder.Eq{"logout_unix": 0}, builder.Lt{"last_access_unix": abandonedCutoff}),
),
).Delete(&UserSession{})
return err
}
// DeleteUserSessionsByUserID removes all session records for a user (used on user deletion)
func DeleteUserSessionsByUserID(ctx context.Context, userID int64) error {
_, err := db.GetEngine(ctx).Where("user_id = ?", userID).Delete(&UserSession{})
return err
}