Compare commits
2
Commits
c78a473e59
...
2983c89b42
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2983c89b42 | ||
|
|
a45a6e16bd |
@@ -1,8 +1,11 @@
|
||||
.PHONY: run build test tidy mock-upstream web-dev web-build
|
||||
.PHONY: run build test tidy mock-upstream web-dev web-build reset-admin-password
|
||||
|
||||
run:
|
||||
cd server && go run ./cmd/server
|
||||
|
||||
reset-admin-password:
|
||||
cd server && go run ./cmd/server reset-admin-password
|
||||
|
||||
build:
|
||||
cd server && go build -o bin/openteam ./cmd/server
|
||||
|
||||
|
||||
+2
-2
@@ -275,8 +275,8 @@ Anthropic /v1/messages ──┘
|
||||
|
||||
#### 5.3.3 API Key
|
||||
|
||||
- 格式:`sk-ot` + 48 位随机 base62,**创建时仅展示一次**。
|
||||
- 存储:仅 SHA-256 哈希 + 展示前缀(如 `sk-otaB3c…`);请求时哈希后查表。
|
||||
- 格式:`sk-ot-` + 48 位随机 base62,**创建时仅展示一次**。
|
||||
- 存储:仅 SHA-256 哈希 + 展示前缀(如 `sk-ot-aB3c…`);请求时哈希后查表。
|
||||
- 附加能力:密钥级配额(每日 token / 请求数)、模型白名单、过期时间、启停。
|
||||
- 限额检查用 Redis 计数,与用户级限流叠加。
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
- **限流/配额**(内存计数):密钥级每日请求数 / Token 数配额、用户级每秒速率(`OT_RATELIMIT_USER_RPS`);超限返回 429
|
||||
- **前端**:Dashboard / 管理总览骨架屏加载态
|
||||
- **用户体系**:注册(开放/邀请码可切换,管理后台可改)、登录(JWT access + HttpOnly refresh cookie)、argon2id 密码
|
||||
- **API Key**:`sk-ot` 48 位 base62,仅存 SHA-256 哈希,明文一次性展示;支持限额/过期/白名单字段
|
||||
- **API Key**:`sk-ot-` 48 位 base62,仅存 SHA-256 哈希,明文一次性展示;支持限额/过期/白名单字段
|
||||
- **用量计费**:请求级 `usage_logs` 异步批量落库,按模型价格扣减余额,日粒度预聚合(`usage_daily`)
|
||||
- **管理 API**:渠道 CRUD + 连通测试 + 模型导入、模型管理 + 定价 + 渠道绑定、用户管理、全局用量/统计、系统配置
|
||||
- **前端**(Vue3 + Tailwind,taste-skill 设计,深色优先)
|
||||
|
||||
@@ -20,6 +20,12 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 子命令分发:`openteam reset-admin-password ...`(无子命令时启动服务)
|
||||
if len(os.Args) > 1 && os.Args[1] == "reset-admin-password" {
|
||||
runResetAdminPassword(os.Args[2:])
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("config: %v", err)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// 子命令 reset-admin-password:重置管理员密码(忘记密码时用,需在 server/ 目录或配置可加载处执行)。
|
||||
// 用法:go run ./cmd/server reset-admin-password [-user admin] [-password 新密码]
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
|
||||
"github.com/openteam/server/internal/config"
|
||||
"github.com/openteam/server/internal/pkg/crypto"
|
||||
"github.com/openteam/server/internal/store"
|
||||
)
|
||||
|
||||
// pwAlphabet 密码随机串字符集(去掉 0/O/1/l/I 等易混淆字符)。
|
||||
const pwAlphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz"
|
||||
|
||||
func runResetAdminPassword(args []string) {
|
||||
fs := flag.NewFlagSet("reset-admin-password", flag.ExitOnError)
|
||||
user := fs.String("user", "admin", "目标管理员用户名")
|
||||
password := fs.String("password", "", "新密码(至少 8 位;留空则随机生成并打印一次)")
|
||||
_ = fs.Parse(args)
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("config: %v", err)
|
||||
}
|
||||
db, err := store.Open(cfg.DB.Driver, cfg.DB.DSN)
|
||||
if err != nil {
|
||||
log.Fatalf("db: %v", err)
|
||||
}
|
||||
|
||||
var u store.User
|
||||
if err := db.Where("username = ? AND role = ?", *user, store.RoleAdmin).First(&u).Error; err != nil {
|
||||
log.Fatalf("admin user %q not found", *user)
|
||||
}
|
||||
|
||||
pw := *password
|
||||
generated := false
|
||||
if pw == "" {
|
||||
pw, err = randomPassword(16)
|
||||
if err != nil {
|
||||
log.Fatalf("generate password: %v", err)
|
||||
}
|
||||
generated = true
|
||||
}
|
||||
if len(pw) < 8 || len(pw) > 72 {
|
||||
log.Fatal("password length must be between 8 and 72")
|
||||
}
|
||||
|
||||
hasher := crypto.NewPasswordHasher(cfg.Auth.Argon2Time, cfg.Auth.Argon2Memory, cfg.Auth.Argon2Threads, cfg.Auth.Argon2KeyLen, cfg.Auth.SaltLen)
|
||||
hash, err := hasher.HashPassword(pw)
|
||||
if err != nil {
|
||||
log.Fatalf("hash password: %v", err)
|
||||
}
|
||||
if err := db.Model(&store.User{}).Where("id = ?", u.ID).Update("password_hash", hash).Error; err != nil {
|
||||
log.Fatalf("update password: %v", err)
|
||||
}
|
||||
|
||||
if generated {
|
||||
fmt.Printf("已重置 %q 的密码:%s\n", u.Username, pw)
|
||||
fmt.Println("(请立即保存,不会再次显示)")
|
||||
} else {
|
||||
fmt.Printf("已重置 %q 的密码。\n", u.Username)
|
||||
}
|
||||
}
|
||||
|
||||
// randomPassword 用 crypto/rand 从 pwAlphabet 生成 n 位随机密码。
|
||||
func randomPassword(n int) (string, error) {
|
||||
buf := make([]byte, n)
|
||||
for i := range buf {
|
||||
k, err := rand.Int(rand.Reader, big.NewInt(int64(len(pwAlphabet))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buf[i] = pwAlphabet[k.Int64()]
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package apikey 生成与管理 API Key:sk-ot + 48 位 base62 随机串。
|
||||
// Package apikey 生成与管理 API Key:sk-ot- + 48 位 base62 随机串。
|
||||
// 库中仅存 SHA-256 哈希与展示前缀(PLANNING §4.3.3)。
|
||||
package apikey
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
const (
|
||||
alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
keyLen = 48
|
||||
prefix = "sk-ot"
|
||||
prefix = "sk-ot-"
|
||||
)
|
||||
|
||||
// Generate 生成明文 key(仅创建时展示一次)与哈希、前缀。
|
||||
@@ -34,7 +34,7 @@ func Hash(key string) string {
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// Prefix 展示前缀:sk-ot…(前 12 字符)
|
||||
// Prefix 展示前缀:sk-ot-aB3c…(前 12 字符)
|
||||
func Prefix(key string) string {
|
||||
if len(key) <= 12 {
|
||||
return key
|
||||
|
||||
@@ -142,7 +142,7 @@ func (g *Gateway) Auth(c *gin.Context) {
|
||||
key = strings.TrimSpace(c.GetHeader("x-api-key"))
|
||||
}
|
||||
if !apikey.Valid(key) {
|
||||
apiError(c, http.StatusUnauthorized, "invalid_api_key", "Invalid API key format. Expected: Authorization: Bearer sk-ot... or x-api-key: sk-ot...")
|
||||
apiError(c, http.StatusUnauthorized, "invalid_api_key", "Invalid API key format. Expected: Authorization: Bearer sk-ot-... or x-api-key: sk-ot-...")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ const baseURL = `${window.location.origin}/v1`
|
||||
<div class="space-y-3 p-4 leading-relaxed">
|
||||
<div>
|
||||
<p class="text-muted"><span class="text-accent">$</span> curl {{ baseURL }}/chat/completions</p>
|
||||
<p class="text-muted"> -H <span class="text-accent">"Authorization: Bearer sk-ot..."</span> \</p>
|
||||
<p class="text-muted"> -H <span class="text-accent">"Authorization: Bearer sk-ot-..."</span> \</p>
|
||||
<p class="text-muted"> -d <span class="text-ink">'{"model": "claude-sonnet-5", "messages": [{"role": "user", "content": "你好"}]}'</span></p>
|
||||
</div>
|
||||
<div class="border-t border-edge pt-3 text-muted">
|
||||
@@ -136,7 +136,7 @@ const baseURL = `${window.location.origin}/v1`
|
||||
</p>
|
||||
</div>
|
||||
<div class="card p-5">
|
||||
<p class="font-mono text-xs text-muted">sk-ot…</p>
|
||||
<p class="font-mono text-xs text-muted">sk-ot-…</p>
|
||||
<h3 class="mt-2 text-lg font-semibold">API Key 管理</h3>
|
||||
<p class="mt-2 text-sm leading-relaxed text-muted">
|
||||
密钥仅存哈希,支持配额、过期与模型白名单,创建时一次性展示。
|
||||
|
||||
Reference in New Issue
Block a user