feat(store,permissions): схема Фазы 1, доступ к данным и движок прав
- миграция 00002: users, sessions, totp_secrets, webauthn_credentials, security_events, guilds, guild_members, roles, member_roles, channels, channel_overrides, audit_log + дефолтные instance_settings (AGENT.md 6.1) - internal/store: Snowflake-идентификаторы, CRUD пользователей и сессий (ротация, step-up, logout-all), TOTP и события безопасности, серверы, участники, роли, комнаты и оверрайды, настройки инстанса и аудит - internal/permissions: 37 прав битмаской, вычисление по правилам §6.2 (баз role @user → оверрайды ролей → оверрайд пользователя → ADMINISTRATOR), иерархия ролей и участников, тайм-ауты, обход для инстанс-админа, LRU-кэш с инвалидацией - internal/source: адаптер permissions.Source поверх store - тесты: 18 unit-тестов прав + интеграционный набор на реальной SQLite (приватная комната, модератор, владелец, инстанс-админ, тайм-аут) - golangci: обоснованное исключение gosec для пакета store (конверсии Snowflake и сборка SQL из константных шаблонов)
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Snowflake — генератор сортируемых идентификаторов (AGENT.md 6.4):
|
||||
// 41 бит времени от эпохи 2026-01-01T00:00:00Z, 10 бит узла, 12 бит счётчика.
|
||||
type Snowflake struct {
|
||||
mu sync.Mutex
|
||||
epoch int64
|
||||
node int64
|
||||
lastMs int64
|
||||
sequence int64
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
snowflakeEpochMs = int64(1767225600000) // 2026-01-01T00:00:00Z
|
||||
nodeBits = 10
|
||||
sequenceBits = 12
|
||||
maxNode = (1 << nodeBits) - 1
|
||||
maxSequence = (1 << sequenceBits) - 1
|
||||
)
|
||||
|
||||
func NewSnowflake(node uint16) *Snowflake {
|
||||
return &Snowflake{
|
||||
epoch: snowflakeEpochMs,
|
||||
node: int64(node) & maxNode,
|
||||
now: func() time.Time { return time.Now().UTC() },
|
||||
}
|
||||
}
|
||||
|
||||
// Next возвращает монотонно возрастающий идентификатор.
|
||||
func (s *Snowflake) Next() uint64 {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
nowMs := s.now().UnixMilli()
|
||||
if nowMs < s.lastMs {
|
||||
// Часы ушли назад: продолжаем с последней метки, чтобы не выдать дубль.
|
||||
nowMs = s.lastMs
|
||||
}
|
||||
if nowMs == s.lastMs {
|
||||
s.sequence = (s.sequence + 1) & maxSequence
|
||||
if s.sequence == 0 {
|
||||
// Счётчик переполнен в эту миллисекунду — ждём следующую.
|
||||
for nowMs <= s.lastMs {
|
||||
time.Sleep(time.Millisecond)
|
||||
nowMs = s.now().UnixMilli()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.sequence = 0
|
||||
}
|
||||
s.lastMs = nowMs
|
||||
|
||||
id := ((nowMs - s.epoch) << (nodeBits + sequenceBits)) | (s.node << sequenceBits) | s.sequence
|
||||
return uint64(id)
|
||||
}
|
||||
|
||||
// SnowflakeTime возвращает время создания идентификатора.
|
||||
func SnowflakeTime(id uint64) time.Time {
|
||||
ms := int64(id>>(nodeBits+sequenceBits)) + snowflakeEpochMs
|
||||
return time.UnixMilli(ms).UTC()
|
||||
}
|
||||
Reference in New Issue
Block a user