DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,209 @@
|
||||
package accountcenter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"longheng.io/server/internal/platform/httpx"
|
||||
)
|
||||
|
||||
const maxAdminJSONBytes = 1 << 20
|
||||
|
||||
type AdminOptions struct {
|
||||
Prefix string
|
||||
Wrap func(http.HandlerFunc) http.HandlerFunc
|
||||
WrapDangerous func(operation string, next http.HandlerFunc) http.HandlerFunc
|
||||
}
|
||||
|
||||
type AdminAccountView struct {
|
||||
Account Account `json:"account"`
|
||||
Access AccountAccess `json:"access"`
|
||||
}
|
||||
|
||||
func RegisterAdminRoutes(mux *http.ServeMux, center *Center, options AdminOptions) error {
|
||||
if mux == nil {
|
||||
return errors.New("account center admin mux is required")
|
||||
}
|
||||
if center == nil {
|
||||
return errors.New("account center is required")
|
||||
}
|
||||
prefix := strings.TrimRight(strings.TrimSpace(options.Prefix), "/")
|
||||
if prefix == "" {
|
||||
prefix = "/accountcenter"
|
||||
}
|
||||
wrap := options.Wrap
|
||||
if wrap == nil {
|
||||
wrap = func(next http.HandlerFunc) http.HandlerFunc { return next }
|
||||
}
|
||||
wrapDangerous := options.WrapDangerous
|
||||
if wrapDangerous == nil {
|
||||
wrapDangerous = func(_ string, next http.HandlerFunc) http.HandlerFunc { return wrap(next) }
|
||||
}
|
||||
mux.HandleFunc(prefix, wrap(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.Header().Set("Allow", http.MethodGet)
|
||||
httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"snapshot": center.Snapshot(),
|
||||
"state": center.Export(),
|
||||
})
|
||||
}))
|
||||
mux.HandleFunc(prefix+"/account", wrap(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.Header().Set("Allow", http.MethodGet)
|
||||
httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
return
|
||||
}
|
||||
account, ok, err := lookupAdminAccount(r, center)
|
||||
if writeAdminError(w, err) {
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
httpx.WriteJSON(w, http.StatusNotFound, map[string]string{"error": ErrAccountNotFound.Error()})
|
||||
return
|
||||
}
|
||||
access, err := center.AccountAccess(r.Context(), account.ID)
|
||||
if writeAdminError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, AdminAccountView{Account: account, Access: access})
|
||||
}))
|
||||
mux.HandleFunc(prefix+"/login", wrapDangerous("accountcenter.login", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
return
|
||||
}
|
||||
var request LoginRequest
|
||||
if !decodeAdminJSON(w, r, &request) {
|
||||
return
|
||||
}
|
||||
result, err := center.Login(r.Context(), request)
|
||||
if writeAdminError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, result)
|
||||
}))
|
||||
mux.HandleFunc(prefix+"/bind", wrapDangerous("accountcenter.bind", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
return
|
||||
}
|
||||
var request BindRequest
|
||||
if !decodeAdminJSON(w, r, &request) {
|
||||
return
|
||||
}
|
||||
account, err := center.Bind(r.Context(), request)
|
||||
if writeAdminError(w, err) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, account)
|
||||
}))
|
||||
mux.HandleFunc(prefix+"/ban", wrapDangerous("accountcenter.ban", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
return
|
||||
}
|
||||
var record BanRecord
|
||||
if !decodeAdminJSON(w, r, &record) {
|
||||
return
|
||||
}
|
||||
if writeAdminError(w, center.SetBan(r.Context(), record)) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"saved": true})
|
||||
}))
|
||||
mux.HandleFunc(prefix+"/allow", wrapDangerous("accountcenter.allow", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
return
|
||||
}
|
||||
var record AllowRecord
|
||||
if !decodeAdminJSON(w, r, &record) {
|
||||
return
|
||||
}
|
||||
if writeAdminError(w, center.SetAllow(r.Context(), record)) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"saved": true})
|
||||
}))
|
||||
mux.HandleFunc(prefix+"/shards", wrapDangerous("accountcenter.shards", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Shards []Shard `json:"shards"`
|
||||
}
|
||||
if !decodeAdminJSON(w, r, &request) {
|
||||
return
|
||||
}
|
||||
if writeAdminError(w, center.SetShards(r.Context(), request.Shards)) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"saved": len(request.Shards)})
|
||||
}))
|
||||
mux.HandleFunc(prefix+"/gates", wrapDangerous("accountcenter.gates", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
httpx.WriteJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Gates []GateNode `json:"gates"`
|
||||
}
|
||||
if !decodeAdminJSON(w, r, &request) {
|
||||
return
|
||||
}
|
||||
if writeAdminError(w, center.SetGates(r.Context(), request.Gates)) {
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"saved": len(request.Gates)})
|
||||
}))
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupAdminAccount(r *http.Request, center *Center) (Account, bool, error) {
|
||||
query := r.URL.Query()
|
||||
if accountID := strings.TrimSpace(query.Get("account_id")); accountID != "" {
|
||||
return center.GetAccount(r.Context(), accountID)
|
||||
}
|
||||
return center.FindAccountByIdentity(r.Context(), Identity{
|
||||
Kind: query.Get("kind"),
|
||||
Issuer: query.Get("issuer"),
|
||||
Subject: query.Get("subject"),
|
||||
Email: query.Get("email"),
|
||||
Channel: query.Get("channel"),
|
||||
DeviceID: query.Get("device_id"),
|
||||
})
|
||||
}
|
||||
|
||||
func decodeAdminJSON(w http.ResponseWriter, r *http.Request, out any) bool {
|
||||
if err := httpx.DecodeStrictJSON(w, r, maxAdminJSONBytes, out); err != nil {
|
||||
httpx.WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func writeAdminError(w http.ResponseWriter, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
status := http.StatusBadRequest
|
||||
if errors.Is(err, ErrAccountBanned) || errors.Is(err, ErrAllowListRequired) || errors.Is(err, ErrShardClosed) {
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
if errors.Is(err, ErrShardNotFound) || errors.Is(err, ErrGateUnavailable) || errors.Is(err, ErrAccountNotFound) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
httpx.WriteJSON(w, status, map[string]string{"error": err.Error()})
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,791 @@
|
||||
package accountcenter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TokenIssuer func(context.Context, Account, Shard, GateNode) (string, error)
|
||||
|
||||
type CenterOptions struct {
|
||||
Now func() time.Time
|
||||
TokenIssuer TokenIssuer
|
||||
Store Store
|
||||
}
|
||||
|
||||
type Center struct {
|
||||
mu sync.Mutex
|
||||
saveMu sync.Mutex
|
||||
now func() time.Time
|
||||
issuer TokenIssuer
|
||||
store Store
|
||||
version uint64
|
||||
nextNum int64
|
||||
accounts map[string]Account
|
||||
identity map[string]string
|
||||
bans map[string]BanRecord
|
||||
allows map[string]AllowRecord
|
||||
banIndex map[string]map[string]struct{}
|
||||
allowIndex map[string]map[string]struct{}
|
||||
shards map[string]Shard
|
||||
gates map[string]GateNode
|
||||
}
|
||||
|
||||
func NewMemoryCenter(options CenterOptions) *Center {
|
||||
center, err := NewCenter(context.Background(), options)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return center
|
||||
}
|
||||
|
||||
func NewCenter(ctx context.Context, options CenterOptions) (*Center, error) {
|
||||
now := options.Now
|
||||
if now == nil {
|
||||
now = func() time.Time { return time.Now().UTC() }
|
||||
}
|
||||
issuer := options.TokenIssuer
|
||||
if issuer == nil {
|
||||
issuer = randomTokenIssuer
|
||||
}
|
||||
center := &Center{
|
||||
now: now,
|
||||
issuer: issuer,
|
||||
store: options.Store,
|
||||
nextNum: 100000,
|
||||
accounts: make(map[string]Account),
|
||||
identity: make(map[string]string),
|
||||
bans: make(map[string]BanRecord),
|
||||
allows: make(map[string]AllowRecord),
|
||||
banIndex: make(map[string]map[string]struct{}),
|
||||
allowIndex: make(map[string]map[string]struct{}),
|
||||
shards: make(map[string]Shard),
|
||||
gates: make(map[string]GateNode),
|
||||
}
|
||||
if options.Store != nil {
|
||||
exported, ok, err := options.Store.Load(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: load account center state: %w", ErrStoreUnavailable, err)
|
||||
}
|
||||
if ok {
|
||||
center.importLocked(exported)
|
||||
}
|
||||
}
|
||||
return center, nil
|
||||
}
|
||||
|
||||
func (c *Center) Login(ctx context.Context, request LoginRequest) (LoginResult, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
identity, err := normalizeIdentity(request.Identity)
|
||||
if err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
c.saveMu.Lock()
|
||||
c.mu.Lock()
|
||||
account, created, err := c.findOrCreateAcct(identity)
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return LoginResult{}, err
|
||||
}
|
||||
shard, err := c.selectShardLocked(request.PreferredShardID)
|
||||
if err != nil {
|
||||
if created {
|
||||
c.rollbackCreated(account, identity)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return LoginResult{}, err
|
||||
}
|
||||
if err := c.checkAccessLocked(account.ID, shard.ID, request.RequireAllowList); err != nil {
|
||||
if created {
|
||||
c.rollbackCreated(account, identity)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return LoginResult{}, err
|
||||
}
|
||||
gate, err := c.selectGateLocked(account.ID, shard)
|
||||
if err != nil {
|
||||
if created {
|
||||
c.rollbackCreated(account, identity)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return LoginResult{}, err
|
||||
}
|
||||
if created {
|
||||
exported, version := c.exportForSaveLocked()
|
||||
c.mu.Unlock()
|
||||
if err := c.persistAfterUnlock(ctx, exported, version, func() {
|
||||
c.rollbackCreated(account, identity)
|
||||
}); err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
} else {
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
}
|
||||
token, err := c.issuer(ctx, account, shard, gate)
|
||||
if err != nil {
|
||||
return LoginResult{}, fmt.Errorf("%w: %w", ErrLoginTokenUnavailable, err)
|
||||
}
|
||||
return LoginResult{
|
||||
Account: account,
|
||||
Shard: shard,
|
||||
Gate: GateSelection{
|
||||
Address: gate.Address,
|
||||
LoginToken: token,
|
||||
NodeID: gate.NodeID,
|
||||
},
|
||||
Created: created,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Center) Bind(ctx context.Context, request BindRequest) (Account, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
identity, err := normalizeIdentity(request.Identity)
|
||||
if err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
accountID := strings.TrimSpace(request.AccountID)
|
||||
if accountID == "" {
|
||||
return Account{}, ErrAccountNotFound
|
||||
}
|
||||
id := identityID(identity)
|
||||
c.saveMu.Lock()
|
||||
c.mu.Lock()
|
||||
account, ok := c.accounts[accountID]
|
||||
if !ok {
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return Account{}, ErrAccountNotFound
|
||||
}
|
||||
if bound, ok := c.identity[id]; ok && bound != accountID {
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return Account{}, ErrIdentityConflict
|
||||
}
|
||||
if _, ok := c.identity[id]; !ok {
|
||||
previous := cloneAccount(account)
|
||||
account.Bindings = append(account.Bindings, identity)
|
||||
account.UpdatedAt = c.now().UTC()
|
||||
c.accounts[accountID] = cloneAccount(account)
|
||||
c.identity[id] = accountID
|
||||
exported, version := c.exportForSaveLocked()
|
||||
c.mu.Unlock()
|
||||
if err := c.persistAfterUnlock(ctx, exported, version, func() {
|
||||
c.accounts[accountID] = previous
|
||||
delete(c.identity, id)
|
||||
}); err != nil {
|
||||
return Account{}, err
|
||||
}
|
||||
return cloneAccount(account), nil
|
||||
}
|
||||
out := cloneAccount(c.accounts[accountID])
|
||||
c.mu.Unlock()
|
||||
c.saveMu.Unlock()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Center) SetBan(ctx context.Context, record BanRecord) error {
|
||||
subject := strings.TrimSpace(record.Subject)
|
||||
if subject == "" {
|
||||
return ErrAccountNotFound
|
||||
}
|
||||
record.Subject = subject
|
||||
record.Scope = normalizeScope(record.Scope)
|
||||
if record.CreatedAt.IsZero() {
|
||||
record.CreatedAt = c.now().UTC()
|
||||
}
|
||||
c.saveMu.Lock()
|
||||
c.mu.Lock()
|
||||
key := accessKey(record.Subject, record.Scope)
|
||||
previous, hadPrevious := c.bans[key]
|
||||
if hadPrevious {
|
||||
c.unindexAccessLocked(c.banIndex, key, previous.Subject)
|
||||
}
|
||||
c.bans[accessKey(record.Subject, record.Scope)] = cloneBanRecord(record)
|
||||
c.indexAccessLocked(c.banIndex, key, record.Subject)
|
||||
exported, version := c.exportForSaveLocked()
|
||||
c.mu.Unlock()
|
||||
return c.persistAfterUnlock(ctx, exported, version, func() {
|
||||
c.unindexAccessLocked(c.banIndex, key, record.Subject)
|
||||
if hadPrevious {
|
||||
c.bans[key] = previous
|
||||
c.indexAccessLocked(c.banIndex, key, previous.Subject)
|
||||
} else {
|
||||
delete(c.bans, key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Center) SetAllow(ctx context.Context, record AllowRecord) error {
|
||||
subject := strings.TrimSpace(record.Subject)
|
||||
if subject == "" {
|
||||
return ErrAccountNotFound
|
||||
}
|
||||
record.Subject = subject
|
||||
record.Scope = normalizeScope(record.Scope)
|
||||
if record.CreatedAt.IsZero() {
|
||||
record.CreatedAt = c.now().UTC()
|
||||
}
|
||||
c.saveMu.Lock()
|
||||
c.mu.Lock()
|
||||
key := accessKey(record.Subject, record.Scope)
|
||||
previous, hadPrevious := c.allows[key]
|
||||
if hadPrevious {
|
||||
c.unindexAccessLocked(c.allowIndex, key, previous.Subject)
|
||||
}
|
||||
c.allows[key] = cloneAllowRecord(record)
|
||||
c.indexAccessLocked(c.allowIndex, key, record.Subject)
|
||||
exported, version := c.exportForSaveLocked()
|
||||
c.mu.Unlock()
|
||||
return c.persistAfterUnlock(ctx, exported, version, func() {
|
||||
c.unindexAccessLocked(c.allowIndex, key, record.Subject)
|
||||
if hadPrevious {
|
||||
c.allows[key] = previous
|
||||
c.indexAccessLocked(c.allowIndex, key, previous.Subject)
|
||||
} else {
|
||||
delete(c.allows, key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Center) SetShards(ctx context.Context, shards []Shard) error {
|
||||
c.saveMu.Lock()
|
||||
c.mu.Lock()
|
||||
previous := c.shards
|
||||
c.shards = make(map[string]Shard, len(shards))
|
||||
for _, shard := range shards {
|
||||
shard.ID = strings.TrimSpace(shard.ID)
|
||||
if shard.ID == "" {
|
||||
continue
|
||||
}
|
||||
shard.State = strings.TrimSpace(shard.State)
|
||||
if shard.State == "" {
|
||||
shard.State = ShardOpen
|
||||
}
|
||||
if shard.Weight <= 0 {
|
||||
shard.Weight = 1
|
||||
}
|
||||
c.shards[shard.ID] = shard
|
||||
}
|
||||
exported, version := c.exportForSaveLocked()
|
||||
c.mu.Unlock()
|
||||
return c.persistAfterUnlock(ctx, exported, version, func() {
|
||||
c.shards = previous
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Center) SetGates(ctx context.Context, gates []GateNode) error {
|
||||
c.saveMu.Lock()
|
||||
c.mu.Lock()
|
||||
previous := c.gates
|
||||
c.gates = make(map[string]GateNode, len(gates))
|
||||
for _, gate := range gates {
|
||||
gate.NodeID = strings.TrimSpace(gate.NodeID)
|
||||
gate.Address = strings.TrimSpace(gate.Address)
|
||||
if gate.NodeID == "" || gate.Address == "" {
|
||||
continue
|
||||
}
|
||||
if gate.Weight <= 0 {
|
||||
gate.Weight = 1
|
||||
}
|
||||
c.gates[gate.NodeID] = gate
|
||||
}
|
||||
exported, version := c.exportForSaveLocked()
|
||||
c.mu.Unlock()
|
||||
return c.persistAfterUnlock(ctx, exported, version, func() {
|
||||
c.gates = previous
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Center) Snapshot() map[string]any {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return map[string]any{
|
||||
"accounts": len(c.accounts),
|
||||
"bindings": len(c.identity),
|
||||
"bans": len(c.bans),
|
||||
"allows": len(c.allows),
|
||||
"shards": len(c.shards),
|
||||
"gates": len(c.gates),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Center) GetAccount(ctx context.Context, accountID string) (Account, bool, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Account{}, false, err
|
||||
}
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if accountID == "" {
|
||||
return Account{}, false, ErrAccountNotFound
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
account, ok := c.accounts[accountID]
|
||||
if !ok {
|
||||
return Account{}, false, nil
|
||||
}
|
||||
return cloneAccount(account), true, nil
|
||||
}
|
||||
|
||||
func (c *Center) FindAccountByIdentity(ctx context.Context, identity Identity) (Account, bool, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Account{}, false, err
|
||||
}
|
||||
normalized, err := normalizeIdentity(identity)
|
||||
if err != nil {
|
||||
return Account{}, false, err
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
accountID, ok := c.identity[identityID(normalized)]
|
||||
if !ok {
|
||||
return Account{}, false, nil
|
||||
}
|
||||
account, ok := c.accounts[accountID]
|
||||
if !ok {
|
||||
return Account{}, false, nil
|
||||
}
|
||||
return cloneAccount(account), true, nil
|
||||
}
|
||||
|
||||
func (c *Center) AccountAccess(ctx context.Context, accountID string) (AccountAccess, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return AccountAccess{}, err
|
||||
}
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
if accountID == "" {
|
||||
return AccountAccess{}, ErrAccountNotFound
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
access := AccountAccess{}
|
||||
for key := range c.banIndex[accountID] {
|
||||
if record, ok := c.bans[key]; ok {
|
||||
access.Bans = append(access.Bans, cloneBanRecord(record))
|
||||
}
|
||||
}
|
||||
for key := range c.allowIndex[accountID] {
|
||||
if record, ok := c.allows[key]; ok {
|
||||
access.Allows = append(access.Allows, cloneAllowRecord(record))
|
||||
}
|
||||
}
|
||||
sort.Slice(access.Bans, func(i, j int) bool {
|
||||
return accessKey(access.Bans[i].Subject, access.Bans[i].Scope) < accessKey(access.Bans[j].Subject, access.Bans[j].Scope)
|
||||
})
|
||||
sort.Slice(access.Allows, func(i, j int) bool {
|
||||
return accessKey(access.Allows[i].Subject, access.Allows[i].Scope) < accessKey(access.Allows[j].Subject, access.Allows[j].Scope)
|
||||
})
|
||||
return access, nil
|
||||
}
|
||||
|
||||
func (c *Center) Export() Export {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.exportLocked()
|
||||
}
|
||||
|
||||
func (c *Center) exportLocked() Export {
|
||||
out := Export{
|
||||
Accounts: make([]Account, 0, len(c.accounts)),
|
||||
Bans: make([]BanRecord, 0, len(c.bans)),
|
||||
Allows: make([]AllowRecord, 0, len(c.allows)),
|
||||
Shards: make([]Shard, 0, len(c.shards)),
|
||||
Gates: make([]GateNode, 0, len(c.gates)),
|
||||
}
|
||||
for _, account := range c.accounts {
|
||||
out.Accounts = append(out.Accounts, cloneAccount(account))
|
||||
}
|
||||
for _, ban := range c.bans {
|
||||
out.Bans = append(out.Bans, cloneBanRecord(ban))
|
||||
}
|
||||
for _, allow := range c.allows {
|
||||
out.Allows = append(out.Allows, cloneAllowRecord(allow))
|
||||
}
|
||||
for _, shard := range c.shards {
|
||||
out.Shards = append(out.Shards, shard)
|
||||
}
|
||||
for _, gate := range c.gates {
|
||||
out.Gates = append(out.Gates, gate)
|
||||
}
|
||||
sort.Slice(out.Accounts, func(i, j int) bool { return out.Accounts[i].ID < out.Accounts[j].ID })
|
||||
sort.Slice(out.Bans, func(i, j int) bool {
|
||||
return accessKey(out.Bans[i].Subject, out.Bans[i].Scope) < accessKey(out.Bans[j].Subject, out.Bans[j].Scope)
|
||||
})
|
||||
sort.Slice(out.Allows, func(i, j int) bool {
|
||||
return accessKey(out.Allows[i].Subject, out.Allows[i].Scope) < accessKey(out.Allows[j].Subject, out.Allows[j].Scope)
|
||||
})
|
||||
sort.Slice(out.Shards, func(i, j int) bool { return out.Shards[i].ID < out.Shards[j].ID })
|
||||
sort.Slice(out.Gates, func(i, j int) bool { return out.Gates[i].NodeID < out.Gates[j].NodeID })
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *Center) importLocked(exported Export) {
|
||||
c.accounts = make(map[string]Account, len(exported.Accounts))
|
||||
c.identity = make(map[string]string)
|
||||
c.bans = make(map[string]BanRecord, len(exported.Bans))
|
||||
c.allows = make(map[string]AllowRecord, len(exported.Allows))
|
||||
c.banIndex = make(map[string]map[string]struct{})
|
||||
c.allowIndex = make(map[string]map[string]struct{})
|
||||
c.shards = make(map[string]Shard, len(exported.Shards))
|
||||
c.gates = make(map[string]GateNode, len(exported.Gates))
|
||||
c.nextNum = 100000
|
||||
for _, account := range exported.Accounts {
|
||||
account = cloneAccount(account)
|
||||
c.accounts[account.ID] = account
|
||||
if account.NumID > c.nextNum {
|
||||
c.nextNum = account.NumID
|
||||
}
|
||||
for _, identity := range account.Bindings {
|
||||
normalized, err := normalizeIdentity(identity)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
c.identity[identityID(normalized)] = account.ID
|
||||
}
|
||||
}
|
||||
for _, ban := range exported.Bans {
|
||||
ban.Subject = strings.TrimSpace(ban.Subject)
|
||||
if ban.Subject == "" {
|
||||
continue
|
||||
}
|
||||
ban.Scope = normalizeScope(ban.Scope)
|
||||
key := accessKey(ban.Subject, ban.Scope)
|
||||
c.bans[key] = cloneBanRecord(ban)
|
||||
c.indexAccessLocked(c.banIndex, key, ban.Subject)
|
||||
}
|
||||
for _, allow := range exported.Allows {
|
||||
allow.Subject = strings.TrimSpace(allow.Subject)
|
||||
if allow.Subject == "" {
|
||||
continue
|
||||
}
|
||||
allow.Scope = normalizeScope(allow.Scope)
|
||||
key := accessKey(allow.Subject, allow.Scope)
|
||||
c.allows[key] = cloneAllowRecord(allow)
|
||||
c.indexAccessLocked(c.allowIndex, key, allow.Subject)
|
||||
}
|
||||
for _, shard := range exported.Shards {
|
||||
shard.ID = strings.TrimSpace(shard.ID)
|
||||
if shard.ID == "" {
|
||||
continue
|
||||
}
|
||||
if shard.State == "" {
|
||||
shard.State = ShardOpen
|
||||
}
|
||||
c.shards[shard.ID] = shard
|
||||
}
|
||||
for _, gate := range exported.Gates {
|
||||
gate.NodeID = strings.TrimSpace(gate.NodeID)
|
||||
if gate.NodeID == "" {
|
||||
continue
|
||||
}
|
||||
c.gates[gate.NodeID] = gate
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Center) exportForSaveLocked() (Export, uint64) {
|
||||
c.version++
|
||||
return c.exportLocked(), c.version
|
||||
}
|
||||
|
||||
// saveMu 固定先于 mu 获取,避免落盘失败回滚覆盖后续已提交写入。
|
||||
func (c *Center) persistAfterUnlock(ctx context.Context, exported Export, version uint64, rollback func()) error {
|
||||
defer c.saveMu.Unlock()
|
||||
if c.store == nil {
|
||||
return nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := c.store.Save(ctx, exported); err != nil {
|
||||
c.mu.Lock()
|
||||
if c.version == version && rollback != nil {
|
||||
rollback()
|
||||
c.version++
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return fmt.Errorf("%w: save account center state: %w", ErrStoreUnavailable, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Center) Close() error {
|
||||
if c == nil || c.store == nil {
|
||||
return nil
|
||||
}
|
||||
c.saveMu.Lock()
|
||||
defer c.saveMu.Unlock()
|
||||
if closer, ok := c.store.(interface{ Close() error }); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Center) findOrCreateAcct(identity Identity) (Account, bool, error) {
|
||||
id := identityID(identity)
|
||||
if accountID, ok := c.identity[id]; ok {
|
||||
return cloneAccount(c.accounts[accountID]), false, nil
|
||||
}
|
||||
now := c.now().UTC()
|
||||
c.nextNum++
|
||||
account := Account{
|
||||
ID: fmt.Sprintf("acc-%d", c.nextNum),
|
||||
NumID: c.nextNum,
|
||||
Bindings: []Identity{identity},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
c.accounts[account.ID] = account
|
||||
c.identity[id] = account.ID
|
||||
return cloneAccount(account), true, nil
|
||||
}
|
||||
|
||||
func (c *Center) rollbackCreated(account Account, identity Identity) {
|
||||
delete(c.accounts, account.ID)
|
||||
delete(c.identity, identityID(identity))
|
||||
if account.NumID == c.nextNum {
|
||||
c.nextNum--
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Center) indexAccessLocked(index map[string]map[string]struct{}, key, subject string) {
|
||||
subject = strings.TrimSpace(subject)
|
||||
if subject == "" {
|
||||
return
|
||||
}
|
||||
items := index[subject]
|
||||
if items == nil {
|
||||
items = make(map[string]struct{})
|
||||
index[subject] = items
|
||||
}
|
||||
items[key] = struct{}{}
|
||||
}
|
||||
|
||||
func (c *Center) unindexAccessLocked(index map[string]map[string]struct{}, key, subject string) {
|
||||
subject = strings.TrimSpace(subject)
|
||||
if subject == "" {
|
||||
return
|
||||
}
|
||||
items := index[subject]
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
delete(items, key)
|
||||
if len(items) == 0 {
|
||||
delete(index, subject)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Center) selectShardLocked(preferred string) (Shard, error) {
|
||||
preferred = strings.TrimSpace(preferred)
|
||||
if preferred != "" {
|
||||
shard, ok := c.shards[preferred]
|
||||
if !ok {
|
||||
return Shard{}, ErrShardNotFound
|
||||
}
|
||||
if shard.State == ShardClosed {
|
||||
return Shard{}, ErrShardClosed
|
||||
}
|
||||
return shard, nil
|
||||
}
|
||||
shards := make([]Shard, 0, len(c.shards))
|
||||
for _, shard := range c.shards {
|
||||
if shard.State != ShardClosed {
|
||||
shards = append(shards, shard)
|
||||
}
|
||||
}
|
||||
if len(shards) == 0 {
|
||||
return Shard{}, ErrShardNotFound
|
||||
}
|
||||
sort.Slice(shards, func(i, j int) bool {
|
||||
if shards[i].Weight != shards[j].Weight {
|
||||
return shards[i].Weight > shards[j].Weight
|
||||
}
|
||||
return shards[i].ID < shards[j].ID
|
||||
})
|
||||
return shards[0], nil
|
||||
}
|
||||
|
||||
func (c *Center) selectGateLocked(accountID string, shard Shard) (GateNode, error) {
|
||||
candidates := make([]GateNode, 0, len(c.gates))
|
||||
for _, gate := range c.gates {
|
||||
if gate.Draining || gate.Address == "" {
|
||||
continue
|
||||
}
|
||||
if shard.GateGroupID != "" && gate.GateGroupID != "" && shard.GateGroupID != gate.GateGroupID {
|
||||
continue
|
||||
}
|
||||
if len(gate.ShardIDs) > 0 && !containsString(gate.ShardIDs, shard.ID) {
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, gate)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return GateNode{}, ErrGateUnavailable
|
||||
}
|
||||
sort.Slice(candidates, func(i, j int) bool { return candidates[i].NodeID < candidates[j].NodeID })
|
||||
idx := int(hashString(accountID+"|"+shard.ID) % uint64(len(candidates))) //nolint:gosec // G115:取模结果严格小于 candidates 长度,可安全作为 slice 下标。
|
||||
return candidates[idx], nil
|
||||
}
|
||||
|
||||
func (c *Center) checkAccessLocked(accountID, shardID string, requireAllow bool) error {
|
||||
now := c.now().UTC()
|
||||
for _, scope := range []string{"global", "shard:" + shardID} {
|
||||
if ban, ok := c.bans[accessKey(accountID, scope)]; ok {
|
||||
if ban.Until.IsZero() || ban.Until.After(now) {
|
||||
return ErrAccountBanned
|
||||
}
|
||||
}
|
||||
}
|
||||
if !requireAllow {
|
||||
return nil
|
||||
}
|
||||
if _, ok := c.allows[accessKey(accountID, "global")]; ok {
|
||||
return nil
|
||||
}
|
||||
if _, ok := c.allows[accessKey(accountID, "shard:"+shardID)]; ok {
|
||||
return nil
|
||||
}
|
||||
return ErrAllowListRequired
|
||||
}
|
||||
|
||||
func normalizeIdentity(identity Identity) (Identity, error) {
|
||||
identity.Kind = strings.ToLower(strings.TrimSpace(identity.Kind))
|
||||
identity.Issuer = strings.TrimSpace(identity.Issuer)
|
||||
identity.Subject = strings.TrimSpace(identity.Subject)
|
||||
identity.Email = strings.ToLower(strings.TrimSpace(identity.Email))
|
||||
identity.Channel = strings.ToLower(strings.TrimSpace(identity.Channel))
|
||||
identity.DeviceID = strings.TrimSpace(identity.DeviceID)
|
||||
switch identity.Kind {
|
||||
case IdentityDevice:
|
||||
if identity.Subject == "" {
|
||||
identity.Subject = identity.DeviceID
|
||||
}
|
||||
case IdentityEmail:
|
||||
if identity.Subject == "" {
|
||||
identity.Subject = identity.Email
|
||||
}
|
||||
case IdentityOIDC:
|
||||
if identity.Issuer == "" || identity.Subject == "" {
|
||||
return Identity{}, ErrInvalidIdentity
|
||||
}
|
||||
case IdentityChannel:
|
||||
if identity.Issuer == "" {
|
||||
identity.Issuer = identity.Channel
|
||||
}
|
||||
}
|
||||
if identity.Kind == "" || identity.Subject == "" {
|
||||
return Identity{}, ErrInvalidIdentity
|
||||
}
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
func identityID(identity Identity) string {
|
||||
identity, _ = normalizeIdentity(identity)
|
||||
return "v1:" + encodeIdentityPart(identity.Kind) + ":" + encodeIdentityPart(identity.Issuer) + ":" + encodeIdentityPart(identity.Subject)
|
||||
}
|
||||
|
||||
func encodeIdentityPart(value string) string {
|
||||
if value == "" {
|
||||
return "_"
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(value))
|
||||
}
|
||||
|
||||
func normalizeScope(scope string) string {
|
||||
scope = strings.TrimSpace(scope)
|
||||
if scope == "" {
|
||||
return "global"
|
||||
}
|
||||
return scope
|
||||
}
|
||||
|
||||
func accessKey(subject, scope string) string {
|
||||
return "v1:" + encodeIdentityPart(strings.TrimSpace(subject)) + ":" + encodeIdentityPart(normalizeScope(scope))
|
||||
}
|
||||
|
||||
func containsString(values []string, target string) bool {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hashString(value string) uint64 {
|
||||
h := fnv.New64a()
|
||||
_, _ = h.Write([]byte(value))
|
||||
return h.Sum64()
|
||||
}
|
||||
|
||||
func randomTokenIssuer(_ context.Context, account Account, shard Shard, _ GateNode) (string, error) {
|
||||
var buf [16]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "lh-login-" + account.ID + "-" + shard.ID + "-" + hex.EncodeToString(buf[:]), nil
|
||||
}
|
||||
|
||||
func cloneAccount(account Account) Account {
|
||||
bindings := make([]Identity, 0, len(account.Bindings))
|
||||
for _, identity := range account.Bindings {
|
||||
bindings = append(bindings, cloneIdentity(identity))
|
||||
}
|
||||
account.Bindings = bindings
|
||||
account.Metadata = cloneStringMap(account.Metadata)
|
||||
return account
|
||||
}
|
||||
|
||||
func cloneIdentity(identity Identity) Identity {
|
||||
identity.Metadata = cloneStringMap(identity.Metadata)
|
||||
return identity
|
||||
}
|
||||
|
||||
func cloneBanRecord(record BanRecord) BanRecord {
|
||||
record.Metadata = cloneStringMap(record.Metadata)
|
||||
return record
|
||||
}
|
||||
|
||||
func cloneAllowRecord(record AllowRecord) AllowRecord {
|
||||
record.Metadata = cloneStringMap(record.Metadata)
|
||||
return record
|
||||
}
|
||||
|
||||
func cloneStringMap(in map[string]string) map[string]string {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(in))
|
||||
for k, v := range in {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ctxErr(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package accountcenter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSQLDBRequired = errors.New("account center sql db is required")
|
||||
ErrInvalidTable = errors.New("account center sql table is invalid")
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSQLTable = "account_center_state"
|
||||
sqlStateName = "default"
|
||||
)
|
||||
|
||||
var sqlTablePattern = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
|
||||
|
||||
type SQLStore struct {
|
||||
db *sql.DB
|
||||
stmts SQLStatements
|
||||
}
|
||||
|
||||
type SQLOptions struct {
|
||||
DB *sql.DB
|
||||
Table string
|
||||
}
|
||||
|
||||
type SQLStatements struct {
|
||||
Table string
|
||||
EnsureSchema string
|
||||
Load string
|
||||
Save string
|
||||
}
|
||||
|
||||
func NewSQLStore(options SQLOptions) (*SQLStore, error) {
|
||||
if options.DB == nil {
|
||||
return nil, ErrSQLDBRequired
|
||||
}
|
||||
stmts, err := NewSQLStatements(options.Table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SQLStore{db: options.DB, stmts: stmts}, nil
|
||||
}
|
||||
|
||||
func NewSQLStatements(table string) (SQLStatements, error) {
|
||||
table, err := normalizeSQLTable(table)
|
||||
if err != nil {
|
||||
return SQLStatements{}, err
|
||||
}
|
||||
quoted := "`" + table + "`"
|
||||
return SQLStatements{
|
||||
Table: table,
|
||||
EnsureSchema: fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
|
||||
name VARCHAR(64) NOT NULL,
|
||||
payload LONGTEXT NOT NULL,
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`, quoted),
|
||||
Load: fmt.Sprintf(`SELECT payload FROM %s WHERE name=?`, quoted),
|
||||
Save: fmt.Sprintf(`INSERT INTO %s (name, payload) VALUES (?, ?) ON DUPLICATE KEY UPDATE payload=VALUES(payload), updated_at=CURRENT_TIMESTAMP(6)`, quoted),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) EnsureSchema(ctx context.Context) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return ErrSQLDBRequired
|
||||
}
|
||||
_, err := s.db.ExecContext(ctx, s.stmts.EnsureSchema)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SQLStore) Load(ctx context.Context) (Export, bool, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Export{}, false, err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return Export{}, false, ErrSQLDBRequired
|
||||
}
|
||||
var payload []byte
|
||||
if err := s.db.QueryRowContext(ctx, s.stmts.Load, sqlStateName).Scan(&payload); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return Export{}, false, nil
|
||||
}
|
||||
return Export{}, false, err
|
||||
}
|
||||
var exported Export
|
||||
if err := json.Unmarshal(payload, &exported); err != nil {
|
||||
return Export{}, false, err
|
||||
}
|
||||
return exported, true, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) Save(ctx context.Context, exported Export) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return ErrSQLDBRequired
|
||||
}
|
||||
payload, err := json.Marshal(exported)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.db.ExecContext(ctx, s.stmts.Save, sqlStateName, payload)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SQLStore) Close() error {
|
||||
if s == nil || s.db == nil {
|
||||
return nil
|
||||
}
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
func normalizeSQLTable(table string) (string, error) {
|
||||
table = strings.TrimSpace(table)
|
||||
if table == "" {
|
||||
table = defaultSQLTable
|
||||
}
|
||||
if !sqlTablePattern.MatchString(table) {
|
||||
return "", ErrInvalidTable
|
||||
}
|
||||
return table, nil
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package accountcenter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidIdentity = errors.New("account center identity is invalid")
|
||||
ErrAccountNotFound = errors.New("account center account not found")
|
||||
ErrIdentityConflict = errors.New("account center identity is already bound to another account")
|
||||
ErrAccountBanned = errors.New("account center account is banned")
|
||||
ErrAllowListRequired = errors.New("account center allowlist is required")
|
||||
ErrShardNotFound = errors.New("account center shard not found")
|
||||
ErrShardClosed = errors.New("account center shard is closed")
|
||||
ErrGateUnavailable = errors.New("account center gate is unavailable")
|
||||
ErrLoginTokenUnavailable = errors.New("account center login token is unavailable")
|
||||
ErrStoreUnavailable = errors.New("account center store is unavailable")
|
||||
)
|
||||
|
||||
const (
|
||||
IdentityDevice = "device"
|
||||
IdentityEmail = "email"
|
||||
IdentityOIDC = "oidc"
|
||||
IdentityChannel = "channel"
|
||||
)
|
||||
|
||||
const (
|
||||
ShardOpen = "open"
|
||||
ShardMaintaining = "maintaining"
|
||||
ShardClosed = "closed"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
Kind string `json:"kind"`
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
Subject string `json:"subject"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
DeviceID string `json:"device_id,omitempty"`
|
||||
VerifiedAt time.Time `json:"verified_at,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
ID string `json:"id"`
|
||||
NumID int64 `json:"num_id,omitempty"`
|
||||
Bindings []Identity `json:"bindings,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Identity Identity `json:"identity"`
|
||||
PreferredShardID string `json:"preferred_shard_id,omitempty"`
|
||||
RequireAllowList bool `json:"require_allowlist,omitempty"`
|
||||
RemoteAddr string `json:"remote_addr,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type LoginResult struct {
|
||||
Account Account `json:"account"`
|
||||
Shard Shard `json:"shard"`
|
||||
Gate GateSelection `json:"gate"`
|
||||
Created bool `json:"created,omitempty"`
|
||||
}
|
||||
|
||||
type BindRequest struct {
|
||||
AccountID string `json:"account_id"`
|
||||
Identity Identity `json:"identity"`
|
||||
}
|
||||
|
||||
type BanRecord struct {
|
||||
Subject string `json:"subject"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Until time.Time `json:"until,omitempty"`
|
||||
Operator string `json:"operator,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type AllowRecord struct {
|
||||
Subject string `json:"subject"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Operator string `json:"operator,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type Shard struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
State string `json:"state"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
GateGroupID string `json:"gate_group_id,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type GateNode struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Address string `json:"address"`
|
||||
ShardIDs []string `json:"shard_ids,omitempty"`
|
||||
GateGroupID string `json:"gate_group_id,omitempty"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
Draining bool `json:"draining,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type GateSelection struct {
|
||||
Address string `json:"address"`
|
||||
LoginToken string `json:"login_token"`
|
||||
NodeID string `json:"node_id,omitempty"`
|
||||
}
|
||||
|
||||
type Export struct {
|
||||
Accounts []Account `json:"accounts"`
|
||||
Bans []BanRecord `json:"bans"`
|
||||
Allows []AllowRecord `json:"allows"`
|
||||
Shards []Shard `json:"shards"`
|
||||
Gates []GateNode `json:"gates"`
|
||||
}
|
||||
|
||||
type AccountAccess struct {
|
||||
Bans []BanRecord `json:"bans,omitempty"`
|
||||
Allows []AllowRecord `json:"allows,omitempty"`
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
Load(context.Context) (Export, bool, error)
|
||||
Save(context.Context, Export) error
|
||||
}
|
||||
Reference in new issue
Block a user