mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-25 22:46:40 +02:00
141 lines
4.9 KiB
Go
141 lines
4.9 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"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 retentionSeconds ago
|
|
// - Abandoned sessions (never logged out) whose last activity is older than maxLifetime + retentionSeconds
|
|
func CleanupExpiredUserSessions(ctx context.Context, retentionSeconds, maxLifetime int64) error {
|
|
now := int64(timeutil.TimeStampNow())
|
|
logoutCutoff := now - retentionSeconds
|
|
abandonedCutoff := now - maxLifetime - retentionSeconds
|
|
|
|
_, 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
|
|
}
|