mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-19 23:47:45 +08:00
refactor backend API structure
This commit is contained in:
403
internal/httpapi/admin/shared/helpers.go
Normal file
403
internal/httpapi/admin/shared/helpers.go
Normal file
@@ -0,0 +1,403 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ds2api/internal/config"
|
||||
"ds2api/internal/util"
|
||||
)
|
||||
|
||||
var intFrom = util.IntFrom
|
||||
|
||||
var WriteJSON = util.WriteJSON
|
||||
var IntFrom = util.IntFrom
|
||||
|
||||
func ReverseAccounts(a []config.Account) { reverseAccounts(a) }
|
||||
func IntFromQuery(r *http.Request, key string, d int) int {
|
||||
return intFromQuery(r, key, d)
|
||||
}
|
||||
func NilIfEmpty(s string) any { return nilIfEmpty(s) }
|
||||
func NilIfZero(v int64) any { return nilIfZero(v) }
|
||||
func MaskSecretPreview(secret string) string {
|
||||
return maskSecretPreview(secret)
|
||||
}
|
||||
func ToStringSlice(v any) ([]string, bool) { return toStringSlice(v) }
|
||||
func ToAccount(m map[string]any) config.Account {
|
||||
return toAccount(m)
|
||||
}
|
||||
func ToAPIKeys(v any) ([]config.APIKey, bool) {
|
||||
return toAPIKeys(v)
|
||||
}
|
||||
func NormalizeAPIKeyForStorage(item config.APIKey) config.APIKey {
|
||||
return normalizeAPIKeyForStorage(item)
|
||||
}
|
||||
func APIKeyHasMetadata(item config.APIKey) bool {
|
||||
return apiKeyHasMetadata(item)
|
||||
}
|
||||
func MergeAPIKeysPreferStructured(existing, incoming []config.APIKey) ([]config.APIKey, int) {
|
||||
return mergeAPIKeysPreferStructured(existing, incoming)
|
||||
}
|
||||
func MergeAPIKeyRecord(existing, incoming config.APIKey) config.APIKey {
|
||||
return mergeAPIKeyRecord(existing, incoming)
|
||||
}
|
||||
func FieldString(m map[string]any, key string) string {
|
||||
return fieldString(m, key)
|
||||
}
|
||||
func FieldStringOptional(m map[string]any, key string) (string, bool) {
|
||||
return fieldStringOptional(m, key)
|
||||
}
|
||||
func StatusOr(v int, d int) int { return statusOr(v, d) }
|
||||
func AccountMatchesIdentifier(acc config.Account, identifier string) bool {
|
||||
return accountMatchesIdentifier(acc, identifier)
|
||||
}
|
||||
func NormalizeAccountForStorage(acc config.Account) config.Account {
|
||||
return normalizeAccountForStorage(acc)
|
||||
}
|
||||
func ToProxy(m map[string]any) config.Proxy {
|
||||
return toProxy(m)
|
||||
}
|
||||
func FindProxyByID(c config.Config, proxyID string) (config.Proxy, bool) {
|
||||
return findProxyByID(c, proxyID)
|
||||
}
|
||||
func AccountDedupeKey(acc config.Account) string { return accountDedupeKey(acc) }
|
||||
func NormalizeAndDedupeAccounts(accounts []config.Account) []config.Account {
|
||||
return normalizeAndDedupeAccounts(accounts)
|
||||
}
|
||||
func FindAccountByIdentifier(store ConfigStore, identifier string) (config.Account, bool) {
|
||||
return findAccountByIdentifier(store, identifier)
|
||||
}
|
||||
|
||||
func ComputeSyncHash(store ConfigStore) string {
|
||||
if store == nil {
|
||||
return ""
|
||||
}
|
||||
snap := store.Snapshot().Clone()
|
||||
snap.ClearAccountTokens()
|
||||
snap.VercelSyncHash = ""
|
||||
snap.VercelSyncTime = 0
|
||||
b, _ := json.Marshal(snap)
|
||||
sum := md5.Sum(b)
|
||||
return fmt.Sprintf("%x", sum)
|
||||
}
|
||||
|
||||
func SyncHashForJSON(s string) string {
|
||||
var cfg config.Config
|
||||
if err := json.Unmarshal([]byte(s), &cfg); err != nil {
|
||||
return ""
|
||||
}
|
||||
cfg.VercelSyncHash = ""
|
||||
cfg.VercelSyncTime = 0
|
||||
cfg.ClearAccountTokens()
|
||||
b, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
sum := md5.Sum(b)
|
||||
return fmt.Sprintf("%x", sum)
|
||||
}
|
||||
|
||||
func reverseAccounts(a []config.Account) {
|
||||
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
}
|
||||
|
||||
func intFromQuery(r *http.Request, key string, d int) int {
|
||||
v := r.URL.Query().Get(key)
|
||||
if v == "" {
|
||||
return d
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return d
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func nilIfEmpty(s string) any {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func nilIfZero(v int64) any {
|
||||
if v == 0 {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func maskSecretPreview(secret string) string {
|
||||
secret = strings.TrimSpace(secret)
|
||||
if secret == "" {
|
||||
return ""
|
||||
}
|
||||
if len(secret) <= 4 {
|
||||
return strings.Repeat("*", len(secret))
|
||||
}
|
||||
return secret[:2] + "****" + secret[len(secret)-2:]
|
||||
}
|
||||
|
||||
func toStringSlice(v any) ([]string, bool) {
|
||||
arr, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
out := make([]string, 0, len(arr))
|
||||
for _, item := range arr {
|
||||
out = append(out, strings.TrimSpace(fmt.Sprintf("%v", item)))
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
func toAccount(m map[string]any) config.Account {
|
||||
email := fieldString(m, "email")
|
||||
mobile := config.NormalizeMobileForStorage(fieldString(m, "mobile"))
|
||||
return config.Account{
|
||||
Name: fieldString(m, "name"),
|
||||
Remark: fieldString(m, "remark"),
|
||||
Email: email,
|
||||
Mobile: mobile,
|
||||
Password: fieldString(m, "password"),
|
||||
ProxyID: fieldString(m, "proxy_id"),
|
||||
}
|
||||
}
|
||||
|
||||
func toAPIKeys(v any) ([]config.APIKey, bool) {
|
||||
arr, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
out := make([]config.APIKey, 0, len(arr))
|
||||
seen := map[string]struct{}{}
|
||||
for _, item := range arr {
|
||||
switch x := item.(type) {
|
||||
case map[string]any:
|
||||
key := fieldString(x, "key")
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, config.APIKey{
|
||||
Key: key,
|
||||
Name: fieldString(x, "name"),
|
||||
Remark: fieldString(x, "remark"),
|
||||
})
|
||||
default:
|
||||
key := strings.TrimSpace(fmt.Sprintf("%v", item))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, config.APIKey{Key: key})
|
||||
}
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
func normalizeAPIKeyForStorage(item config.APIKey) config.APIKey {
|
||||
return config.APIKey{
|
||||
Key: strings.TrimSpace(item.Key),
|
||||
Name: strings.TrimSpace(item.Name),
|
||||
Remark: strings.TrimSpace(item.Remark),
|
||||
}
|
||||
}
|
||||
|
||||
func apiKeyHasMetadata(item config.APIKey) bool {
|
||||
return strings.TrimSpace(item.Name) != "" || strings.TrimSpace(item.Remark) != ""
|
||||
}
|
||||
|
||||
func mergeAPIKeysPreferStructured(existing, incoming []config.APIKey) ([]config.APIKey, int) {
|
||||
if len(existing) == 0 && len(incoming) == 0 {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
merged := make([]config.APIKey, 0, len(existing)+len(incoming))
|
||||
index := make(map[string]int, len(existing)+len(incoming))
|
||||
for _, item := range existing {
|
||||
item = normalizeAPIKeyForStorage(item)
|
||||
if item.Key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := index[item.Key]; ok {
|
||||
continue
|
||||
}
|
||||
index[item.Key] = len(merged)
|
||||
merged = append(merged, item)
|
||||
}
|
||||
|
||||
imported := 0
|
||||
for _, item := range incoming {
|
||||
item = normalizeAPIKeyForStorage(item)
|
||||
if item.Key == "" {
|
||||
continue
|
||||
}
|
||||
if idx, ok := index[item.Key]; ok {
|
||||
keep := merged[idx]
|
||||
next := mergeAPIKeyRecord(keep, item)
|
||||
if next != keep {
|
||||
merged[idx] = next
|
||||
imported++
|
||||
}
|
||||
continue
|
||||
}
|
||||
index[item.Key] = len(merged)
|
||||
merged = append(merged, item)
|
||||
imported++
|
||||
}
|
||||
|
||||
if len(merged) == 0 {
|
||||
return nil, imported
|
||||
}
|
||||
return merged, imported
|
||||
}
|
||||
|
||||
func mergeAPIKeyRecord(existing, incoming config.APIKey) config.APIKey {
|
||||
existing = normalizeAPIKeyForStorage(existing)
|
||||
incoming = normalizeAPIKeyForStorage(incoming)
|
||||
if existing.Key == "" {
|
||||
return incoming
|
||||
}
|
||||
if apiKeyHasMetadata(existing) {
|
||||
return existing
|
||||
}
|
||||
if apiKeyHasMetadata(incoming) {
|
||||
return incoming
|
||||
}
|
||||
return existing
|
||||
}
|
||||
|
||||
func fieldString(m map[string]any, key string) string {
|
||||
v, ok := m[key]
|
||||
if !ok || v == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf("%v", v))
|
||||
}
|
||||
|
||||
func fieldStringOptional(m map[string]any, key string) (string, bool) {
|
||||
v, ok := m[key]
|
||||
if !ok || v == nil {
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf("%v", v)), true
|
||||
}
|
||||
|
||||
func statusOr(v int, d int) int {
|
||||
if v == 0 {
|
||||
return d
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func accountMatchesIdentifier(acc config.Account, identifier string) bool {
|
||||
id := strings.TrimSpace(identifier)
|
||||
if id == "" {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(acc.Email) == id {
|
||||
return true
|
||||
}
|
||||
if mobileKey := config.CanonicalMobileKey(id); mobileKey != "" && mobileKey == config.CanonicalMobileKey(acc.Mobile) {
|
||||
return true
|
||||
}
|
||||
return acc.Identifier() == id
|
||||
}
|
||||
|
||||
func normalizeAccountForStorage(acc config.Account) config.Account {
|
||||
acc.Name = strings.TrimSpace(acc.Name)
|
||||
acc.Remark = strings.TrimSpace(acc.Remark)
|
||||
acc.Email = strings.TrimSpace(acc.Email)
|
||||
acc.Mobile = config.NormalizeMobileForStorage(acc.Mobile)
|
||||
acc.ProxyID = strings.TrimSpace(acc.ProxyID)
|
||||
return acc
|
||||
}
|
||||
|
||||
func toProxy(m map[string]any) config.Proxy {
|
||||
return config.NormalizeProxy(config.Proxy{
|
||||
ID: fieldString(m, "id"),
|
||||
Name: fieldString(m, "name"),
|
||||
Type: fieldString(m, "type"),
|
||||
Host: fieldString(m, "host"),
|
||||
Port: intFrom(m["port"]),
|
||||
Username: fieldString(m, "username"),
|
||||
Password: fieldString(m, "password"),
|
||||
})
|
||||
}
|
||||
|
||||
func findProxyByID(c config.Config, proxyID string) (config.Proxy, bool) {
|
||||
id := strings.TrimSpace(proxyID)
|
||||
if id == "" {
|
||||
return config.Proxy{}, false
|
||||
}
|
||||
for _, proxy := range c.Proxies {
|
||||
proxy = config.NormalizeProxy(proxy)
|
||||
if proxy.ID == id {
|
||||
return proxy, true
|
||||
}
|
||||
}
|
||||
return config.Proxy{}, false
|
||||
}
|
||||
|
||||
func accountDedupeKey(acc config.Account) string {
|
||||
if email := strings.TrimSpace(acc.Email); email != "" {
|
||||
return "email:" + email
|
||||
}
|
||||
if mobile := config.CanonicalMobileKey(acc.Mobile); mobile != "" {
|
||||
return "mobile:" + mobile
|
||||
}
|
||||
if id := strings.TrimSpace(acc.Identifier()); id != "" {
|
||||
return "id:" + id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeAndDedupeAccounts(accounts []config.Account) []config.Account {
|
||||
if len(accounts) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]config.Account, 0, len(accounts))
|
||||
seen := make(map[string]struct{}, len(accounts))
|
||||
for _, acc := range accounts {
|
||||
acc = normalizeAccountForStorage(acc)
|
||||
key := accountDedupeKey(acc)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, acc)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func findAccountByIdentifier(store ConfigStore, identifier string) (config.Account, bool) {
|
||||
id := strings.TrimSpace(identifier)
|
||||
if id == "" {
|
||||
return config.Account{}, false
|
||||
}
|
||||
if acc, ok := store.FindAccount(id); ok {
|
||||
return acc, true
|
||||
}
|
||||
accounts := store.Snapshot().Accounts
|
||||
for _, acc := range accounts {
|
||||
if accountMatchesIdentifier(acc, id) {
|
||||
return acc, true
|
||||
}
|
||||
}
|
||||
return config.Account{}, false
|
||||
}
|
||||
Reference in New Issue
Block a user