Files
new-api-custom/model/login_verification.go
T

90 lines
3.1 KiB
Go

package model
import (
"time"
"github.com/QuantumNous/new-api/common"
"gorm.io/gorm"
)
// UserVerificationState is an authoritative, credential-free projection for
// choosing an authentication method. Loading it never fetches credential secrets.
type UserVerificationState struct {
UserID int
Status int
Role int
AuthVersion int64
HasPassword bool
HasTwoFA bool
TwoFALocked bool
HasPasskey bool
}
func GetUserVerificationState(userID int) (*UserVerificationState, error) {
return getUserVerificationState(DB, userID, false)
}
func getUserVerificationState(tx *gorm.DB, userID int, forUpdate bool) (*UserVerificationState, error) {
if userID <= 0 {
return nil, ErrUserSessionInvalid
}
var state UserVerificationState
query := tx.Model(&User{}).Select(
"id AS user_id, status, role, auth_version, CASE WHEN password <> '' THEN 1 ELSE 0 END AS has_password, "+
"EXISTS (?) AS has_two_fa, EXISTS (?) AS two_fa_locked, EXISTS (?) AS has_passkey",
tx.Model(&TwoFA{}).Select("1").Where("user_id = ? AND is_enabled = ?", userID, true),
tx.Model(&TwoFA{}).Select("1").Where("user_id = ? AND is_enabled = ? AND locked_until > ?", userID, true, time.Now()),
tx.Model(&PasskeyCredential{}).Select("1").Where("user_id = ?", userID),
).Where("id = ?", userID)
if forUpdate {
query = lockForUpdate(query)
}
if err := query.Take(&state).Error; err != nil {
return nil, err
}
return &state, nil
}
// CreateUserSessionFromLoginFlow commits the one-time login authorization and
// the resulting session together. The user lock serializes credential changes
// and session issuance, including the per-user session limits.
func CreateUserSessionFromLoginFlow(token string, session *UserSession, validate func(*AuthFlow, *UserVerificationState) error) error {
if session == nil || validate == nil {
return ErrUserSessionInvalid
}
cacheDeadline := userSessionCacheDeadline()
_, err := ConsumeAuthFlowWithAction(token, AuthFlowMatch{
Purpose: AuthFlowPurposeLoginVerification, UserId: session.UserID,
}, func(tx *gorm.DB, flow *AuthFlow) error {
state, err := getUserVerificationState(tx, session.UserID, true)
if err != nil {
return err
}
if state.Status != common.UserStatusEnabled || state.AuthVersion != session.UserAuthVersion {
return ErrUserSessionInactive
}
if err := validate(flow, state); err != nil {
return err
}
now := time.Now().Unix()
var activeCount, issuanceCount int64
if err := tx.Model(&UserSession{}).Where("user_id = ? AND status = ? AND expires_at > ?", session.UserID, UserSessionStatusActive, now).Count(&activeCount).Error; err != nil {
return err
}
if activeCount >= int64(common.UserSessionActiveLimit) {
return ErrUserSessionLimit
}
if err := tx.Model(&UserSession{}).Where("user_id = ? AND created_at > ?", session.UserID, now-common.UserSessionIssuanceWindowSeconds).Count(&issuanceCount).Error; err != nil {
return err
}
if issuanceCount >= int64(common.UserSessionIssuanceLimit) {
return ErrUserSessionIssuanceLimit
}
return createUserSessionWithTx(tx, session)
})
if err != nil {
return err
}
return publishCreatedUserSession(session, cacheDeadline)
}