Files
Agent-System/internal/auth/session.go
T
weichenandCursor 6219caf690 feat(authz): complete Phase 1 admin roles, sessions, and unified admin gate
Add roles CRUD backed by system_roles tables, session list/revoke APIs,
capabilities fallback for memory mode, and route all admin checks through
AuthorizationService instead of hardcoded role-admin checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:22 +00:00

131 lines
3.6 KiB
Go

package auth
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"time"
)
var errSessionNotFound = errors.New("session not found or expired")
const (
defaultIdleSessionTTL = 24 * time.Hour
defaultAbsoluteSessionTTL = 7 * 24 * time.Hour
)
// SessionMeta 创建会话时的客户端元数据(可选)。
type SessionMeta struct {
IPHash string
UserAgent string
}
// SessionRecord 用户可见的活跃会话摘要。
type SessionRecord struct {
ID string `json:"id"`
CreatedAt string `json:"created_at"`
LastSeenAt string `json:"last_seen_at"`
UserAgent string `json:"user_agent"`
IsCurrent bool `json:"is_current"`
}
// SessionStore 持久化或内存会话存储。
type SessionStore interface {
CreateSession(ctx context.Context, userID int64, tokenHash string, meta SessionMeta) error
ResolveUserID(ctx context.Context, tokenHash string) (int64, error)
TouchSession(ctx context.Context, tokenHash string) error
RevokeByTokenHash(ctx context.Context, tokenHash string) error
RevokeAllForUser(ctx context.Context, userID int64) error
ListActiveSessions(ctx context.Context, userID int64, currentTokenHash string) ([]SessionRecord, error)
RevokeSessionByID(ctx context.Context, userID int64, sessionID string) error
}
// HashToken 将会话 token 转为存储用 SHA-256 十六进制摘要。
func HashToken(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}
type memorySessionEntry struct {
id string
userID int64
createdAt time.Time
lastSeenAt time.Time
userAgent string
}
// MemorySessionStore 进程内 token 映射(仅 dev/stub fallback)。
type MemorySessionStore struct {
tokens map[string]memorySessionEntry
}
func NewMemorySessionStore() *MemorySessionStore {
return &MemorySessionStore{tokens: make(map[string]memorySessionEntry)}
}
func (s *MemorySessionStore) CreateSession(_ context.Context, userID int64, tokenHash string, meta SessionMeta) error {
now := time.Now()
s.tokens[tokenHash] = memorySessionEntry{
id: tokenHash[:12],
userID: userID,
createdAt: now,
lastSeenAt: now,
userAgent: meta.UserAgent,
}
return nil
}
func (s *MemorySessionStore) ResolveUserID(_ context.Context, tokenHash string) (int64, error) {
entry, ok := s.tokens[tokenHash]
if !ok {
return 0, errSessionNotFound
}
return entry.userID, nil
}
func (s *MemorySessionStore) TouchSession(_ context.Context, _ string) error {
return nil
}
func (s *MemorySessionStore) RevokeByTokenHash(_ context.Context, tokenHash string) error {
delete(s.tokens, tokenHash)
return nil
}
func (s *MemorySessionStore) RevokeAllForUser(_ context.Context, userID int64) error {
for tok, entry := range s.tokens {
if entry.userID == userID {
delete(s.tokens, tok)
}
}
return nil
}
func (s *MemorySessionStore) ListActiveSessions(_ context.Context, userID int64, currentTokenHash string) ([]SessionRecord, error) {
out := make([]SessionRecord, 0)
for tok, entry := range s.tokens {
if entry.userID != userID {
continue
}
out = append(out, SessionRecord{
ID: entry.id,
CreatedAt: entry.createdAt.Format("2006-01-02 15:04:05"),
LastSeenAt: entry.lastSeenAt.Format("2006-01-02 15:04:05"),
UserAgent: entry.userAgent,
IsCurrent: currentTokenHash != "" && tok == currentTokenHash,
})
}
return out, nil
}
func (s *MemorySessionStore) RevokeSessionByID(_ context.Context, userID int64, sessionID string) error {
for tok, entry := range s.tokens {
if entry.userID == userID && entry.id == sessionID {
delete(s.tokens, tok)
return nil
}
}
return errSessionNotFound
}