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>
131 lines
3.6 KiB
Go
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
|
|
}
|