610 lines
21 KiB
Go
610 lines
21 KiB
Go
package store
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"crypto/sha256"
|
||
"crypto/subtle"
|
||
"database/sql"
|
||
"encoding/base64"
|
||
"encoding/hex"
|
||
"errors"
|
||
"strings"
|
||
"time"
|
||
|
||
"golang.org/x/crypto/argon2"
|
||
)
|
||
|
||
// ---- 认证与租户(阶段 1:API key 认证,user 即租户 owner)----
|
||
// key 明文仅创建时返回一次;库中只存 SHA-256 摘要(数据可靠性标准:不落明文凭证)。
|
||
|
||
// User 是租户账户(owner 全局唯一;id 供后续扩展用户目录)。
|
||
type User struct {
|
||
ID string `json:"id"`
|
||
Owner string `json:"owner"`
|
||
Name string `json:"name"`
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
}
|
||
|
||
// APIKeyInfo 是密钥的展示视图(不含摘要)。
|
||
type APIKeyInfo struct {
|
||
Name string `json:"name"`
|
||
Owner string `json:"owner"`
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
||
Revoked bool `json:"revoked"`
|
||
}
|
||
|
||
func hashKey(plain string) string {
|
||
sum := sha256.Sum256([]byte(plain))
|
||
return hex.EncodeToString(sum[:])
|
||
}
|
||
|
||
// EnsureUser 幂等创建租户账户(owner 唯一)。
|
||
func (s *Store) EnsureUser(owner, name string) error {
|
||
_, err := s.db.Exec(s.q(`INSERT INTO users(id,owner,name,created_at) VALUES(?,?,?,?)
|
||
ON CONFLICT(owner) DO NOTHING`), newID("usr"), owner, name, nowISO())
|
||
return err
|
||
}
|
||
|
||
func (s *Store) GetUser(owner string) (User, error) {
|
||
var u User
|
||
var ca string
|
||
err := s.db.QueryRow(s.q(`SELECT id,owner,name,created_at FROM users WHERE owner=?`), owner).Scan(&u.ID, &u.Owner, &u.Name, &ca)
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return u, ErrNotFound
|
||
}
|
||
if err != nil {
|
||
return u, err
|
||
}
|
||
u.CreatedAt, _ = parseTime(ca)
|
||
return u, nil
|
||
}
|
||
|
||
func (s *Store) ListUsers() ([]User, error) {
|
||
rows, err := s.db.Query(s.q(`SELECT id,owner,name,created_at FROM users ORDER BY created_at`))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := []User{}
|
||
for rows.Next() {
|
||
var u User
|
||
var ca string
|
||
if err := rows.Scan(&u.ID, &u.Owner, &u.Name, &ca); err != nil {
|
||
return nil, err
|
||
}
|
||
u.CreatedAt, _ = parseTime(ca)
|
||
out = append(out, u)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
// CreateAPIKey 创建密钥:明文 `ask_<hex32>` 仅此一次返回。
|
||
func (s *Store) CreateAPIKey(owner, name string) (string, error) {
|
||
raw := make([]byte, 16)
|
||
if _, err := rand.Read(raw); err != nil {
|
||
return "", err
|
||
}
|
||
plain := "ask_" + hex.EncodeToString(raw)
|
||
if name == "" {
|
||
name = "key-" + nowISO()[:10]
|
||
}
|
||
tx, err := s.db.Begin()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer tx.Rollback()
|
||
if err := s.dialect.lockAccount(tx, owner); err != nil {
|
||
return "", err
|
||
}
|
||
// The account row lock and key insert share a transaction with disable.
|
||
// The version predicate also protects the admission decision if the row
|
||
// changes between the read and the insert on a different dialect.
|
||
var stateVersion int64
|
||
var enabled bool
|
||
var initializationState string
|
||
query := s.q(`SELECT state_version,enabled,initialization_state FROM users WHERE owner=?`)
|
||
if s.Dialect() == "postgres" {
|
||
query += ` FOR UPDATE`
|
||
}
|
||
err = tx.QueryRow(query, owner).Scan(&stateVersion, &enabled, &initializationState)
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return "", ErrAccountForbidden
|
||
}
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if !enabled || (initializationState != InitializationInitialized && initializationState != InitializationLegacy) {
|
||
return "", ErrAccountForbidden
|
||
}
|
||
result, err := tx.Exec(s.q(`INSERT INTO api_keys(key_hash,owner,name,created_at,revoked)
|
||
SELECT ?,owner,?,?,? FROM users WHERE owner=? AND state_version=? AND enabled=?
|
||
AND initialization_state IN (?,?)`),
|
||
hashKey(plain), name, nowISO(), 0, owner, stateVersion, true,
|
||
InitializationInitialized, InitializationLegacy)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if n, err := result.RowsAffected(); err != nil {
|
||
return "", err
|
||
} else if n != 1 {
|
||
return "", ErrAccountForbidden
|
||
}
|
||
if err := tx.Commit(); err != nil {
|
||
return "", err
|
||
}
|
||
return plain, nil
|
||
}
|
||
|
||
// RevokeAPIKeyByName 撤销指定名称的未撤销密钥(返回是否命中)。
|
||
func (s *Store) RevokeAPIKeyByName(owner, name string) (bool, error) {
|
||
res, err := s.db.Exec(s.q(`UPDATE api_keys SET revoked=1 WHERE owner=? AND name=? AND revoked=0`), owner, name)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
n, _ := res.RowsAffected()
|
||
return n > 0, nil
|
||
}
|
||
|
||
func (s *Store) ListAPIKeys(owner string) ([]APIKeyInfo, error) {
|
||
rows, err := s.db.Query(s.q(`SELECT name,owner,created_at,last_used_at,revoked FROM api_keys WHERE owner=? ORDER BY created_at`), owner)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := []APIKeyInfo{}
|
||
for rows.Next() {
|
||
var k APIKeyInfo
|
||
var ca string
|
||
var last sql.NullString
|
||
if err := rows.Scan(&k.Name, &k.Owner, &ca, &last, &k.Revoked); err != nil {
|
||
return nil, err
|
||
}
|
||
k.CreatedAt, _ = parseTime(ca)
|
||
if last.Valid && last.String != "" {
|
||
t, _ := parseTime(last.String)
|
||
k.LastUsedAt = &t
|
||
}
|
||
out = append(out, k)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
// AuthOwner 校验 API key:命中返回 owner;未命中/已撤销返回 false。
|
||
// last_used_at 尽力刷新(失败不影响认证结果)。
|
||
func (s *Store) AuthOwner(plainKey string) (string, bool) {
|
||
if plainKey == "" {
|
||
return "", false
|
||
}
|
||
var owner string
|
||
var revoked int
|
||
err := s.db.QueryRow(s.q(`SELECT k.owner,k.revoked FROM api_keys k JOIN users u ON u.owner=k.owner
|
||
WHERE k.key_hash=? AND u.enabled=?`), hashKey(plainKey), true).Scan(&owner, &revoked)
|
||
if err != nil || revoked == 1 {
|
||
return "", false
|
||
}
|
||
s.db.Exec(s.q(`UPDATE api_keys SET last_used_at=? WHERE key_hash=?`), nowISO(), hashKey(plainKey))
|
||
return owner, true
|
||
}
|
||
|
||
// ---- 账号密码与会话(阶段 2:管理员发放制,2026-09-30 裁定)----
|
||
// 无自助注册:账号由管理员(或 CLI 引导)创建;初始/重置密码后 must_change_password=1,
|
||
// 首次登录强制改密。会话为服务端状态:库存 SHA-256 摘要,cookie 只发一次明文。
|
||
|
||
const sessionTTL = 7 * 24 * time.Hour
|
||
|
||
// AdminUser 管理员用户列表的展示视图(不含哈希)。
|
||
type AdminUser struct {
|
||
ID string `json:"userId"`
|
||
Owner string `json:"owner"`
|
||
Name string `json:"name"`
|
||
Enabled bool `json:"enabled"`
|
||
StateVersion int64 `json:"stateVersion"`
|
||
ProfileVersion int64 `json:"profileVersion"`
|
||
InitializationState string `json:"initializationState"`
|
||
InitializationGeneration int64 `json:"initializationGeneration"`
|
||
IsAdmin bool `json:"isAdmin"`
|
||
IsSuperAdmin bool `json:"isSuperAdmin"`
|
||
MustChangePassword bool `json:"mustChangePassword"`
|
||
HasPassword bool `json:"hasPassword"`
|
||
PasswordSetAt *time.Time `json:"passwordSetAt,omitempty"`
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
}
|
||
|
||
// HashPassword 生成 argon2id PHC 串(m=64MiB,t=2,p=2;随机盐)。
|
||
func HashPassword(password string) (string, error) {
|
||
salt := make([]byte, 16)
|
||
if _, err := rand.Read(salt); err != nil {
|
||
return "", err
|
||
}
|
||
key := argon2.IDKey([]byte(password), salt, 2, 64*1024, 2, 32)
|
||
return "$argon2id$v=19$m=65536,t=2,p=2$" +
|
||
base64.RawStdEncoding.EncodeToString(salt) + "$" +
|
||
base64.RawStdEncoding.EncodeToString(key), nil
|
||
}
|
||
|
||
// VerifyPassword 校验 PHC 串(常量时间比较;坏格式按不匹配处理)。
|
||
func VerifyPassword(hash, password string) bool {
|
||
parts := strings.Split(hash, "$")
|
||
if len(parts) != 6 || parts[1] != "argon2id" {
|
||
return false
|
||
}
|
||
var m uint32 = 65536
|
||
var t uint32 = 2
|
||
var p uint8 = 2
|
||
for _, kv := range strings.Split(parts[3], ",") {
|
||
kv2 := strings.SplitN(kv, "=", 2)
|
||
if len(kv2) != 2 {
|
||
continue
|
||
}
|
||
var v int
|
||
for _, c := range kv2[1] {
|
||
v = v*10 + int(c-'0')
|
||
}
|
||
switch kv2[0] {
|
||
case "m":
|
||
m = uint32(v)
|
||
case "t":
|
||
t = uint32(v)
|
||
case "p":
|
||
p = uint8(v)
|
||
}
|
||
}
|
||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||
if err != nil {
|
||
return false
|
||
}
|
||
want, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||
if err != nil {
|
||
return false
|
||
}
|
||
got := argon2.IDKey([]byte(password), salt, t, m, p, uint32(len(want)))
|
||
return subtle.ConstantTimeCompare(got, want) == 1
|
||
}
|
||
|
||
// CreateAccount 管理员建号:单事务整行 INSERT,owner 唯一冲突返回 ErrConflict。
|
||
// passwordHash 非空时 must_change=1;空哈希表示未设密(走激活 Key),must_change=0。
|
||
// (原子化:旧实现先查后插再 UPDATE,两个并发 create 同名会在 UPDATE 步互相覆盖
|
||
// 角色与密码——R5/KTD12 并发建号不覆盖。)
|
||
func (s *Store) CreateAccount(owner, name, passwordHash string, isAdmin bool) error {
|
||
_, err := s.CreateActivatedAccount(AccountWrite{
|
||
Owner: owner, Name: name, PasswordHash: passwordHash, IsAdmin: isAdmin,
|
||
Action: "account.create",
|
||
})
|
||
return err
|
||
}
|
||
|
||
// SetUserPassword 设置/重置密码:must_change=1(首登强制改密)。
|
||
func (s *Store) SetUserPassword(owner, passwordHash string) error {
|
||
return s.AdminResetUserPassword(owner, passwordHash, "legacy-store", "legacy-store")
|
||
}
|
||
|
||
// ClearMustChangePassword 改密完成后清标志(改密者会话保留,其余吊销由调用方处理)。
|
||
func (s *Store) ClearMustChangePassword(owner string) error {
|
||
_, err := s.db.Exec(s.q(`UPDATE users SET must_change_password=? WHERE owner=?`), false, owner)
|
||
return err
|
||
}
|
||
|
||
// UserAuthState 登录与身份视图。
|
||
type UserAuthState struct {
|
||
UserID string
|
||
Owner string
|
||
Name string
|
||
IsAdmin bool
|
||
IsSuperAdmin bool
|
||
MustChangePassword bool
|
||
HasPassword bool
|
||
Enabled bool
|
||
StateVersion int64
|
||
AuthVersion int
|
||
CredentialOrigin CredentialOrigin
|
||
InitializationState string
|
||
InitializationGeneration int64
|
||
}
|
||
|
||
// GetUserAuthState 按 owner 取认证态(含会话/APIOwner 路径共用)。
|
||
func (s *Store) GetUserAuthState(owner string) (UserAuthState, error) {
|
||
a, err := s.GetAccountByOwner(owner)
|
||
if err != nil {
|
||
return UserAuthState{}, err
|
||
}
|
||
return UserAuthState{
|
||
UserID: a.ID, Owner: a.Owner, Name: a.Profile.Name, IsAdmin: a.IsAdmin, IsSuperAdmin: a.IsSuperAdmin,
|
||
MustChangePassword: a.MustChangePassword, HasPassword: a.HasPassword,
|
||
Enabled: a.Enabled, StateVersion: a.StateVersion, AuthVersion: a.AuthVersion,
|
||
CredentialOrigin: a.CredentialOrigin, InitializationState: a.InitializationState,
|
||
InitializationGeneration: a.InitializationGeneration,
|
||
}, nil
|
||
}
|
||
|
||
// VerifyLogin 校验用户名密码。
|
||
func (s *Store) VerifyLogin(owner, password string) (UserAuthState, bool) {
|
||
st, _, ok := s.VerifyLoginWithVersion(owner, password)
|
||
return st, ok
|
||
}
|
||
|
||
// VerifyLoginWithVersion 校验用户名密码并返回验证所依据的认证版本;调用方建会话
|
||
// 时必须以 CreateSessionVerified 传回该版本,堵「验证在途 → 恢复/改密完成 →
|
||
// 旧请求又建会话」的竞态窗口。
|
||
func (s *Store) VerifyLoginWithVersion(owner, password string) (UserAuthState, int, bool) {
|
||
return s.VerifyLoginWithFallback(owner, password, nil)
|
||
}
|
||
|
||
// FallbackCredential reports whether password equals the in-memory fallback
|
||
// credential configured for owner (KTD2). Implementations must compare in
|
||
// constant time and must never persist or log the secret.
|
||
type FallbackCredential func(owner, password string) bool
|
||
|
||
// verifyAccountCredential checks the database hash first, then the fallback.
|
||
// The fallback is honoured only for an account carrying the persisted
|
||
// super-admin marker, so it can never authenticate any other account.
|
||
func verifyAccountCredential(hash string, isSuperAdmin bool, owner, password string, fallback FallbackCredential) bool {
|
||
if hash != "" && VerifyPassword(hash, password) {
|
||
return true
|
||
}
|
||
return isSuperAdmin && fallback != nil && fallback(owner, password)
|
||
}
|
||
|
||
// VerifyLoginWithFallback 同 VerifyLoginWithVersion,另对已启用且带超级管理员标记的
|
||
// 账户接受 fallback 凭据(R6);数据库密码优先。返回的认证版本语义不变。
|
||
func (s *Store) VerifyLoginWithFallback(owner, password string, fallback FallbackCredential) (UserAuthState, int, bool) {
|
||
var hash string
|
||
var ver int
|
||
var isSuperAdmin bool
|
||
err := s.db.QueryRow(s.q(`SELECT password_hash,auth_version,is_super_admin FROM users WHERE owner=? AND enabled=?`), owner, true).Scan(&hash, &ver, &isSuperAdmin)
|
||
if err != nil || !verifyAccountCredential(hash, isSuperAdmin, owner, password, fallback) {
|
||
return UserAuthState{}, 0, false
|
||
}
|
||
st, err := s.GetUserAuthState(owner)
|
||
if err != nil {
|
||
return UserAuthState{}, 0, false
|
||
}
|
||
return st, ver, true
|
||
}
|
||
|
||
// ---- 会话 ----
|
||
|
||
// CreateSession 建会话:明文 token 仅此一次返回;库存 SHA-256 摘要与到期时间;scope=full。
|
||
func (s *Store) CreateSession(owner string) (string, error) {
|
||
return s.CreateSessionScoped(owner, SessionScopeFull, sessionTTL)
|
||
}
|
||
|
||
// CreateSessionScoped 按 scope 与 TTL 建会话(尽力绑定账户当前认证版本)。
|
||
func (s *Store) CreateSessionScoped(owner, scope string, ttl time.Duration) (string, error) {
|
||
if scope == "" {
|
||
scope = SessionScopeFull
|
||
}
|
||
if ttl <= 0 {
|
||
ttl = sessionTTL
|
||
}
|
||
raw := make([]byte, 32)
|
||
if _, err := rand.Read(raw); err != nil {
|
||
return "", err
|
||
}
|
||
token := "sess_" + hex.EncodeToString(raw)
|
||
exp := time.Now().Add(ttl)
|
||
tx, err := s.db.Begin()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer tx.Rollback()
|
||
if err := s.dialect.lockAccount(tx, owner); err != nil {
|
||
return "", err
|
||
}
|
||
var ver int
|
||
var enabled bool
|
||
query := s.q(`SELECT auth_version,enabled FROM users WHERE owner=?`)
|
||
if s.Dialect() == "postgres" {
|
||
query += ` FOR UPDATE`
|
||
}
|
||
if err := tx.QueryRow(query, owner).Scan(&ver, &enabled); errors.Is(err, sql.ErrNoRows) {
|
||
return "", ErrNotFound
|
||
} else if err != nil {
|
||
return "", err
|
||
}
|
||
if !enabled {
|
||
return "", ErrAccountForbidden
|
||
}
|
||
_, err = tx.Exec(s.q(`INSERT INTO sessions(token_hash,owner,created_at,expires_at,last_seen_at,scope,auth_version) VALUES(?,?,?,?,?,?,?)`),
|
||
hashKey(token), owner, nowISO(), exp.UTC().Format(time.RFC3339Nano), nowISO(), scope, ver)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return token, tx.Commit()
|
||
}
|
||
|
||
// CreateSessionVerified 建会话并绑定期望认证版本:事务内重读账户版本,与密码验证
|
||
// 时的一致才插入;版本已前进(并发恢复/改密/重置)返回 ErrConflict,不建会话。
|
||
func (s *Store) CreateSessionVerified(owner, scope string, ttl time.Duration, expectVersion int) (string, error) {
|
||
if scope == "" {
|
||
scope = SessionScopeFull
|
||
}
|
||
if ttl <= 0 {
|
||
ttl = sessionTTL
|
||
}
|
||
raw := make([]byte, 32)
|
||
if _, err := rand.Read(raw); err != nil {
|
||
return "", err
|
||
}
|
||
token := "sess_" + hex.EncodeToString(raw)
|
||
exp := time.Now().Add(ttl)
|
||
tx, err := s.db.Begin()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer tx.Rollback()
|
||
if err := s.dialect.lockAccount(tx, owner); err != nil {
|
||
return "", err
|
||
}
|
||
var ver int
|
||
var enabled bool
|
||
var origin CredentialOrigin
|
||
versionQuery := s.q(`SELECT auth_version,enabled,credential_origin FROM users WHERE owner=?`)
|
||
if s.Dialect() == "postgres" {
|
||
versionQuery += ` FOR UPDATE`
|
||
}
|
||
err = tx.QueryRow(versionQuery, owner).Scan(&ver, &enabled, &origin)
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return "", ErrNotFound
|
||
}
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if !enabled || ver != expectVersion {
|
||
return "", ErrConflict
|
||
}
|
||
if _, err := tx.Exec(s.q(`INSERT INTO sessions(token_hash,owner,created_at,expires_at,last_seen_at,scope,auth_version,auth_method,credential_origin)
|
||
VALUES(?,?,?,?,?,?,?,?,?)`),
|
||
hashKey(token), owner, nowISO(), exp.UTC().Format(time.RFC3339Nano), nowISO(), scope, ver, AuthMethodPassword, origin); err != nil {
|
||
return "", err
|
||
}
|
||
return token, tx.Commit()
|
||
}
|
||
|
||
// CreateSessionVerifiedWithPasswordLoginCleanup creates a password session
|
||
// bound to expectVersion and atomically applies post-login OTP cleanup.
|
||
func (s *Store) CreateSessionVerifiedWithPasswordLoginCleanup(owner, scope string, ttl time.Duration, expectVersion int) (string, error) {
|
||
if scope == "" {
|
||
scope = SessionScopeFull
|
||
}
|
||
if ttl <= 0 {
|
||
ttl = sessionTTL
|
||
}
|
||
raw := make([]byte, 32)
|
||
if _, err := rand.Read(raw); err != nil {
|
||
return "", err
|
||
}
|
||
token := "sess_" + hex.EncodeToString(raw)
|
||
exp := time.Now().Add(ttl)
|
||
tx, err := s.db.Begin()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer tx.Rollback()
|
||
if err := s.dialect.lockAccount(tx, owner); err != nil {
|
||
return "", err
|
||
}
|
||
var ver int
|
||
var enabled bool
|
||
var origin CredentialOrigin
|
||
versionQuery := s.q(`SELECT auth_version,enabled,credential_origin FROM users WHERE owner=?`)
|
||
if s.Dialect() == "postgres" {
|
||
versionQuery += ` FOR UPDATE`
|
||
}
|
||
err = tx.QueryRow(versionQuery, owner).Scan(&ver, &enabled, &origin)
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return "", ErrNotFound
|
||
}
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if !enabled || ver != expectVersion {
|
||
return "", ErrConflict
|
||
}
|
||
if _, err := tx.Exec(s.q(`INSERT INTO sessions(token_hash,owner,created_at,expires_at,last_seen_at,scope,auth_version,auth_method,credential_origin)
|
||
VALUES(?,?,?,?,?,?,?,?,?)`),
|
||
hashKey(token), owner, nowISO(), exp.UTC().Format(time.RFC3339Nano), nowISO(), scope, ver, AuthMethodPassword, origin); err != nil {
|
||
return "", err
|
||
}
|
||
if _, err := tx.Exec(s.q(`UPDATE login_keys SET status=? WHERE owner=? AND purpose=? AND status=?`),
|
||
LoginKeyStatusRevoked, owner, LoginKeyPurposeOneTimePassword, LoginKeyStatusUnused); err != nil {
|
||
return "", err
|
||
}
|
||
if _, err := tx.Exec(s.q(`DELETE FROM sessions WHERE owner=? AND auth_method=? AND scope=?`),
|
||
owner, AuthMethodOneTimePassword, SessionScopeSetPasswordOnly); err != nil {
|
||
return "", err
|
||
}
|
||
return token, tx.Commit()
|
||
}
|
||
|
||
// SessionOwner 校验会话:命中且未过期返回 owner(尽力刷新 last_seen)。
|
||
func (s *Store) SessionOwner(token string) (string, bool) {
|
||
owner, _, ok := s.SessionInfo(token)
|
||
return owner, ok
|
||
}
|
||
|
||
// SessionAuthMethod reports persisted authentication lineage for an already
|
||
// checked cookie. Callers must still validate current scope/version/effective
|
||
// state with SessionInfo before granting any capability.
|
||
func (s *Store) SessionAuthMethod(token string) (string, error) {
|
||
if token == "" {
|
||
return "", ErrNotFound
|
||
}
|
||
var method string
|
||
err := s.db.QueryRow(s.q(`SELECT auth_method FROM sessions WHERE token_hash=?`), hashKey(token)).Scan(&method)
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return "", ErrNotFound
|
||
}
|
||
return method, err
|
||
}
|
||
|
||
// SessionInfo 返回 owner 与 scope。
|
||
func (s *Store) SessionInfo(token string) (owner, scope string, ok bool) {
|
||
if token == "" {
|
||
return "", "", false
|
||
}
|
||
var exp string
|
||
var sessionVersion, accountVersion int
|
||
var enabled bool
|
||
err := s.db.QueryRow(s.q(`SELECT s.owner,s.expires_at,COALESCE(s.scope,'full'),s.auth_version,u.auth_version,u.enabled
|
||
FROM sessions s JOIN users u ON u.owner=s.owner WHERE s.token_hash=?`), hashKey(token)).
|
||
Scan(&owner, &exp, &scope, &sessionVersion, &accountVersion, &enabled)
|
||
if err != nil {
|
||
return "", "", false
|
||
}
|
||
if !enabled || sessionVersion != accountVersion {
|
||
_, _ = s.db.Exec(s.q(`DELETE FROM sessions WHERE token_hash=? AND auth_version=?`), hashKey(token), sessionVersion)
|
||
return "", "", false
|
||
}
|
||
if expT, err := time.Parse(time.RFC3339Nano, exp); err != nil || time.Now().After(expT) {
|
||
s.db.Exec(s.q(`DELETE FROM sessions WHERE token_hash=?`), hashKey(token))
|
||
return "", "", false
|
||
}
|
||
s.db.Exec(s.q(`UPDATE sessions SET last_seen_at=? WHERE token_hash=?`), nowISO(), hashKey(token))
|
||
if scope == "" {
|
||
scope = SessionScopeFull
|
||
}
|
||
return owner, scope, true
|
||
}
|
||
|
||
// DeleteSession 注销当前会话。
|
||
func (s *Store) DeleteSession(token string) {
|
||
s.db.Exec(s.q(`DELETE FROM sessions WHERE token_hash=?`), hashKey(token))
|
||
}
|
||
|
||
// DeleteUserSessions 吊销用户全部会话(改密/重置后);except 非空时保留该会话。
|
||
func (s *Store) DeleteUserSessions(owner, exceptToken string) {
|
||
if exceptToken != "" {
|
||
s.db.Exec(s.q(`DELETE FROM sessions WHERE owner=? AND token_hash<>?`), owner, hashKey(exceptToken))
|
||
return
|
||
}
|
||
s.db.Exec(s.q(`DELETE FROM sessions WHERE owner=?`), owner)
|
||
}
|
||
|
||
// ListUsersAdmin 管理员用户列表(按创建序)。
|
||
func (s *Store) ListUsersAdmin() ([]AdminUser, error) {
|
||
rows, err := s.db.Query(s.q(`SELECT id,owner,COALESCE(name,''),enabled,state_version,profile_version,initialization_state,initialization_generation,
|
||
password_hash,must_change_password,is_admin,is_super_admin,password_set_at,created_at FROM users ORDER BY created_at`))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := []AdminUser{}
|
||
for rows.Next() {
|
||
var u AdminUser
|
||
var hash, psa, ca string
|
||
if err := rows.Scan(&u.ID, &u.Owner, &u.Name, &u.Enabled, &u.StateVersion, &u.ProfileVersion,
|
||
&u.InitializationState, &u.InitializationGeneration, &hash, &u.MustChangePassword, &u.IsAdmin, &u.IsSuperAdmin, &psa, &ca); err != nil {
|
||
return nil, err
|
||
}
|
||
u.HasPassword = hash != ""
|
||
if psa != "" {
|
||
t, _ := time.Parse(time.RFC3339Nano, psa)
|
||
u.PasswordSetAt = &t
|
||
}
|
||
u.CreatedAt, _ = time.Parse(time.RFC3339Nano, ca)
|
||
out = append(out, u)
|
||
}
|
||
return out, rows.Err()
|
||
}
|