Skip to content

Commit 1fd3cee

Browse files
DouDOU-startclaude
andcommitted
feat(web): refine auth and usage flows
Add non-consuming verification-code checks, broaden account search, and polish themed dashboard and usage views for better usability. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ec30ffd commit 1fd3cee

34 files changed

Lines changed: 564 additions & 217 deletions

backend/internal/infra/mailer/verifycode.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,33 @@ func (s *VerifyCodeStore) Generate(email string) string {
4343
return code
4444
}
4545

46-
// Verify 校验验证码,成功后自动删除。
47-
func (s *VerifyCodeStore) Verify(email, code string) bool {
48-
s.mu.Lock()
49-
defer s.mu.Unlock()
50-
46+
// Check 校验验证码,但不消耗验证码。
47+
func (s *VerifyCodeStore) Check(email, code string) bool {
48+
s.mu.RLock()
5149
entry, ok := s.codes[email]
50+
s.mu.RUnlock()
5251
if !ok {
5352
return false
5453
}
5554
if time.Now().After(entry.expiresAt) {
56-
delete(s.codes, email)
55+
s.mu.Lock()
56+
if current, exists := s.codes[email]; exists && current.expiresAt.Equal(entry.expiresAt) {
57+
delete(s.codes, email)
58+
}
59+
s.mu.Unlock()
5760
return false
5861
}
59-
if entry.code != code {
62+
return entry.code == code
63+
}
64+
65+
// Verify 校验验证码,成功后自动删除。
66+
func (s *VerifyCodeStore) Verify(email, code string) bool {
67+
if !s.Check(email, code) {
6068
return false
6169
}
70+
s.mu.Lock()
6271
delete(s.codes, email)
72+
s.mu.Unlock()
6373
return true
6474
}
6575

backend/internal/infra/store/account_store.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"context"
55
"time"
66

7+
"entgo.io/ent/dialect/sql"
8+
"entgo.io/ent/dialect/sql/sqljson"
9+
710
"github.com/DouDOU-start/airgate-core/ent"
811
entaccount "github.com/DouDOU-start/airgate-core/ent/account"
912
entgroup "github.com/DouDOU-start/airgate-core/ent/group"
@@ -23,12 +26,24 @@ func NewAccountStore(db *ent.Client) *AccountStore {
2326
return &AccountStore{db: db}
2427
}
2528

29+
func accountKeywordMatches(keyword string) predicate.Account {
30+
return entaccount.Or(
31+
entaccount.NameContains(keyword),
32+
entaccount.And(
33+
entaccount.TypeEQ("oauth"),
34+
func(s *sql.Selector) {
35+
s.Where(sqljson.StringContains(entaccount.FieldCredentials, keyword, sqljson.Path("email")))
36+
},
37+
),
38+
)
39+
}
40+
2641
// List 查询账号列表。
2742
func (s *AccountStore) List(ctx context.Context, filter appaccount.ListFilter) ([]appaccount.Account, int64, error) {
2843
query := s.db.Account.Query()
2944

3045
if filter.Keyword != "" {
31-
query = query.Where(entaccount.NameContains(filter.Keyword))
46+
query = query.Where(accountKeywordMatches(filter.Keyword))
3247
}
3348
if filter.Platform != "" {
3449
query = query.Where(entaccount.PlatformEQ(filter.Platform))
@@ -72,7 +87,7 @@ func (s *AccountStore) ListAll(ctx context.Context, filter appaccount.ListFilter
7287
query := s.db.Account.Query()
7388

7489
if filter.Keyword != "" {
75-
query = query.Where(entaccount.NameContains(filter.Keyword))
90+
query = query.Where(accountKeywordMatches(filter.Keyword))
7691
}
7792
if filter.Platform != "" {
7893
query = query.Where(entaccount.PlatformEQ(filter.Platform))
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package store
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
_ "github.com/mattn/go-sqlite3"
8+
9+
"github.com/DouDOU-start/airgate-core/ent"
10+
"github.com/DouDOU-start/airgate-core/ent/enttest"
11+
"github.com/DouDOU-start/airgate-core/ent/migrate"
12+
"github.com/DouDOU-start/airgate-core/internal/app/account"
13+
)
14+
15+
func TestAccountStoreKeywordSearchMatchesOAuthEmail(t *testing.T) {
16+
db := enttestOpen(t)
17+
defer func() {
18+
if err := db.Close(); err != nil {
19+
t.Fatalf("close db: %v", err)
20+
}
21+
}()
22+
23+
ctx := context.Background()
24+
if _, err := db.Account.Create().
25+
SetName("Claude Key").
26+
SetPlatform("openai").
27+
SetType("oauth").
28+
SetCredentials(map[string]string{"email": "claude@example.com", "access_token": "token"}).
29+
Save(ctx); err != nil {
30+
t.Fatalf("create oauth account: %v", err)
31+
}
32+
if _, err := db.Account.Create().
33+
SetName("Other Key").
34+
SetPlatform("openai").
35+
SetType("apikey").
36+
SetCredentials(map[string]string{"api_key": "sk-test"}).
37+
Save(ctx); err != nil {
38+
t.Fatalf("create api key account: %v", err)
39+
}
40+
41+
store := NewAccountStore(db)
42+
items, total, err := store.List(ctx, account.ListFilter{Page: 1, PageSize: 20, Keyword: "claude@"})
43+
if err != nil {
44+
t.Fatalf("List returned error: %v", err)
45+
}
46+
if total != 1 {
47+
t.Fatalf("total = %d, want 1", total)
48+
}
49+
if len(items) != 1 || items[0].Name != "Claude Key" {
50+
t.Fatalf("items = %+v", items)
51+
}
52+
}
53+
54+
func enttestOpen(t *testing.T) *ent.Client {
55+
t.Helper()
56+
return enttest.Open(t, "sqlite3", "file:account_store?mode=memory&cache=shared&_fk=1", enttest.WithMigrateOptions(migrate.WithGlobalUniqueID(false)))
57+
}

backend/internal/server/dto/auth.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ type SendVerifyCodeReq struct {
3232
Email string `json:"email" binding:"required,email"`
3333
}
3434

35+
type VerifyCodeReq struct {
36+
Email string `json:"email" binding:"required,email"`
37+
Code string `json:"code" binding:"required"`
38+
}
39+
3540
// RefreshResp Token 刷新响应
3641
type RefreshResp struct {
3742
Token string `json:"token"`

backend/internal/server/dto/usage.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ type UsageQuery struct {
8585

8686
// UsageFilterQuery 使用记录筛选参数(不含分页,用于聚合统计)
8787
type UsageFilterQuery struct {
88+
APIKeyID *int64 `form:"api_key_id"`
8889
Platform string `form:"platform"`
8990
Model string `form:"model"`
9091
StartDate string `form:"start_date"`

backend/internal/server/handler/auth_handler_routes.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,19 @@ func (h *AuthHandler) Register(c *gin.Context) {
174174
}
175175

176176
// SendVerifyCode 发送邮箱验证码。
177+
func (h *AuthHandler) VerifyCode(c *gin.Context) {
178+
var req dto.VerifyCodeReq
179+
if err := c.ShouldBindJSON(&req); err != nil {
180+
response.BindError(c, err)
181+
return
182+
}
183+
if !h.codeStore.Check(req.Email, req.Code) {
184+
response.BadRequest(c, "验证码无效或已过期")
185+
return
186+
}
187+
response.Success(c, nil)
188+
}
189+
177190
func (h *AuthHandler) SendVerifyCode(c *gin.Context) {
178191
var req dto.SendVerifyCodeReq
179192
if err := c.ShouldBindJSON(&req); err != nil {

backend/internal/server/handler/usage_handler_routes.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,16 @@ func (h *UsageHandler) UserUsageStats(c *gin.Context) {
8181
return
8282
}
8383

84-
// API Key 登录场景:限定统计范围
85-
var scopedKey *int64
84+
apiKeyFilter := query.APIKeyID
8685
scoped := false
8786
if sk := scopedAPIKeyID(c); sk > 0 {
88-
scopedKey = &sk
87+
apiKeyFilter = &sk
8988
scoped = true
9089
}
9190

9291
tz := c.Query("tz")
9392
summary, err := h.service.UserStats(c.Request.Context(), int64(userID), appusage.StatsFilter{
94-
APIKeyID: scopedKey,
93+
APIKeyID: apiKeyFilter,
9594
Platform: query.Platform,
9695
Model: query.Model,
9796
StartDate: query.StartDate,
@@ -109,7 +108,7 @@ func (h *UsageHandler) UserUsageStats(c *gin.Context) {
109108
uid64 := int64(userID)
110109
modelStats, _ := h.service.StatsByModel(c.Request.Context(), appusage.StatsFilter{
111110
UserID: &uid64,
112-
APIKeyID: scopedKey,
111+
APIKeyID: apiKeyFilter,
113112
Platform: query.Platform,
114113
Model: query.Model,
115114
StartDate: query.StartDate,

backend/internal/server/router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func (s *Server) registerRoutes() {
5353
authGroup.POST("/login-apikey", handlers.Auth.LoginByAPIKey)
5454
authGroup.POST("/register", handlers.Auth.Register)
5555
authGroup.POST("/send-verify-code", handlers.Auth.SendVerifyCode)
56+
authGroup.POST("/verify-code", handlers.Auth.VerifyCode)
5657
}
5758

5859
// === 用户路由(需要 JWT 认证) ===

web/src/app/layout/AppShell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ export function AppShell({ children }: AppShellProps) {
342342
<div className="space-y-1 border-t border-border p-3">
343343
{!sidebarCollapsed && (
344344
<Button
345-
className="w-full justify-start"
345+
className="w-full justify-center"
346346
size="sm"
347347
variant="ghost"
348348
onPress={() => { window.location.href = effectiveDocUrl(site.doc_url).href; }}

web/src/app/layout/ChatShell.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ export function ChatShell({ children }: ChatShellProps) {
3434
document.title = site.site_name || 'AirGate';
3535
}, [site.site_name]);
3636

37+
useEffect(() => {
38+
const previousOverflow = document.body.style.overflow;
39+
document.body.style.overflow = 'hidden';
40+
window.scrollTo(0, 0);
41+
return () => {
42+
document.body.style.overflow = previousOverflow;
43+
};
44+
}, []);
45+
3746
const toggleLanguage = () => {
3847
const nextLang = i18n.language === 'zh' ? 'en' : 'zh';
3948
i18n.changeLanguage(nextLang);

0 commit comments

Comments
 (0)