Skip to content

Commit 89412fa

Browse files
authored
feat(User):优化个人中心的页面样式和一些交互 (#864)
* feat(auth): WebAuthn-Anmeldung für discoverable login aktiviert * Entfernt Benutzernamen-Validierung, um discoverable login zu unterstützen * Neue Übersetzung "noWebauthnCredentials" für verschiedene Sprachen hinzugefügt * Escape-Zeichen im ImportModal korrigiert * feat(auth): WebAuthn discoverable login implementieren * `GetUserByWebAuthnCredentialId` Funktion für Benutzerabruf über Credential ID hinzugefügt * Login-Logik für discoverable login ohne Benutzernamen implementiert * Session-Verwaltung für beide Login-Typen angepasst * Fehlerbehandlung und Benutzerprüfung für discoverable login optimiert * feat: Profilseite mit Tabs-Layout umgestaltet * * UI mit Tabs für allgemeine Einstellungen, Bindungen, WebAuthn und Token umgestaltet * Benutzerprofil-Avatar mit animiertem Gradient-Hintergrund hinzugefügt * Passwortbestätigungsfeld und Gruppeninformationen ergänzt * Bindungsansicht mit Avatar-Icons optimiert * feat(profile): Konto-Entbindungsfunktion implementiert - Entbindungsdialog für WeChat, GitHub, Lark und OIDC hinzugefügt - Bestätigungsdialog mit Warnmeldung vor der Entbindung implementiert - Neue Übersetzungen für Entbindungsfunktion in allen Sprachen hinzugefügt * feat(profile): 添加账户解绑功能 - 在路由中添加 `/unbind` POST接口 - 实现 Unbind 控制器支持解绑 GitHub、WeChat、Lark 和 OIDC 绑定 - 添加 UnbindRequest 结构体处理解绑请求参数 - 修复代码格式问题,统一缩进
1 parent 193ffc6 commit 89412fa

File tree

11 files changed

+738
-299
lines changed

11 files changed

+738
-299
lines changed

controller/user.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func setupLogin(user *model.User, c *gin.Context) {
8080
return
8181
}
8282
user.LastLoginTime = time.Now().Unix()
83-
user.LastLoginIp = c.ClientIP()
83+
user.LastLoginIp = c.ClientIP()
8484

8585
user.Update(false)
8686

@@ -809,3 +809,58 @@ func ChangeUserQuota(c *gin.Context) {
809809
"message": "",
810810
})
811811
}
812+
813+
type UnbindRequest struct {
814+
Type string `json:"type"`
815+
}
816+
817+
func Unbind(c *gin.Context) {
818+
var req UnbindRequest
819+
err := json.NewDecoder(c.Request.Body).Decode(&req)
820+
if err != nil {
821+
c.JSON(http.StatusOK, gin.H{
822+
"success": false,
823+
"message": "无效的参数",
824+
})
825+
return
826+
}
827+
id := c.GetInt("id")
828+
user, err := model.GetUserById(id, false)
829+
if err != nil {
830+
c.JSON(http.StatusOK, gin.H{
831+
"success": false,
832+
"message": err.Error(),
833+
})
834+
return
835+
}
836+
updates := make(map[string]interface{})
837+
switch req.Type {
838+
case "github":
839+
updates["github_id"] = ""
840+
updates["github_id_new"] = nil
841+
case "wechat":
842+
updates["wechat_id"] = ""
843+
case "lark":
844+
updates["lark_id"] = ""
845+
case "oidc":
846+
updates["oidc_id"] = ""
847+
default:
848+
c.JSON(http.StatusOK, gin.H{
849+
"success": false,
850+
"message": "未知的绑定类型",
851+
})
852+
return
853+
}
854+
err = model.DB.Model(user).Updates(updates).Error
855+
if err != nil {
856+
c.JSON(http.StatusOK, gin.H{
857+
"success": false,
858+
"message": err.Error(),
859+
})
860+
return
861+
}
862+
c.JSON(http.StatusOK, gin.H{
863+
"success": true,
864+
"message": "",
865+
})
866+
}

controller/webauthn.go

Lines changed: 119 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package controller
22

33
import (
44
"encoding/json"
5+
"errors"
56
"net/http"
67
"one-api/common/webauthn"
78
"one-api/model"
@@ -162,7 +163,7 @@ func WebauthnBeginLogin(c *gin.Context) {
162163

163164
// 从请求中获取用户名
164165
type LoginRequest struct {
165-
Username string `json:"username" binding:"required"`
166+
Username string `json:"username"`
166167
}
167168

168169
var req LoginRequest
@@ -174,30 +175,48 @@ func WebauthnBeginLogin(c *gin.Context) {
174175
return
175176
}
176177

177-
var user = model.User{
178-
Username: req.Username,
179-
}
180-
// 根据用户名获取用户
181-
err = user.FillUserByUsername()
182-
if err != nil {
183-
c.JSON(http.StatusNotFound, gin.H{
184-
"message": "登陆失败",
185-
"success": false,
186-
})
187-
return
188-
}
178+
var options interface{}
179+
var session *wauth.SessionData
189180

190-
options, session, err := webauthnInstance.BeginLogin(&user)
191-
if err != nil {
192-
c.JSON(http.StatusInternalServerError, gin.H{
193-
"message": "无法开始登录: " + err.Error(),
194-
"success": false,
195-
})
196-
return
181+
sess := sessions.Default(c)
182+
183+
if req.Username == "" {
184+
// 无用户名登录 (Discoverable Login)
185+
options, session, err = webauthnInstance.BeginDiscoverableLogin()
186+
if err != nil {
187+
c.JSON(http.StatusInternalServerError, gin.H{
188+
"message": "无法开始无用户名登录: " + err.Error(),
189+
"success": false,
190+
})
191+
return
192+
}
193+
sess.Delete("webauthn_user_id")
194+
} else {
195+
var user = model.User{
196+
Username: req.Username,
197+
}
198+
// 根据用户名获取用户
199+
err = user.FillUserByUsername()
200+
if err != nil {
201+
c.JSON(http.StatusNotFound, gin.H{
202+
"message": "登陆失败",
203+
"success": false,
204+
})
205+
return
206+
}
207+
208+
options, session, err = webauthnInstance.BeginLogin(&user)
209+
if err != nil {
210+
c.JSON(http.StatusInternalServerError, gin.H{
211+
"message": "无法开始登录: " + err.Error(),
212+
"success": false,
213+
})
214+
return
215+
}
216+
sess.Set("webauthn_user_id", strconv.Itoa(user.Id))
197217
}
198218

199219
// 将session存储到用户会话中
200-
sess := sessions.Default(c)
201220
sessionData, err := json.Marshal(session)
202221
if err != nil {
203222
c.JSON(http.StatusInternalServerError, gin.H{
@@ -207,7 +226,6 @@ func WebauthnBeginLogin(c *gin.Context) {
207226
return
208227
}
209228
sess.Set("webauthn_login_session", string(sessionData))
210-
sess.Set("webauthn_user_id", strconv.Itoa(user.Id))
211229
sess.Save()
212230

213231
c.JSON(http.StatusOK, gin.H{
@@ -232,32 +250,14 @@ func WebauthnFinishLogin(c *gin.Context) {
232250
sessionDataStr := sess.Get("webauthn_login_session")
233251
userIdStr := sess.Get("webauthn_user_id")
234252

235-
if sessionDataStr == nil || userIdStr == nil {
253+
if sessionDataStr == nil {
236254
c.JSON(http.StatusBadRequest, gin.H{
237255
"message": "会话已过期",
238256
"success": false,
239257
})
240258
return
241259
}
242260

243-
userId, err := strconv.Atoi(userIdStr.(string))
244-
if err != nil {
245-
c.JSON(http.StatusInternalServerError, gin.H{
246-
"message": "登陆失败",
247-
"success": false,
248-
})
249-
return
250-
}
251-
252-
user, err := model.GetUserById(userId, false)
253-
if err != nil {
254-
c.JSON(http.StatusNotFound, gin.H{
255-
"message": "登陆失败",
256-
"success": false,
257-
})
258-
return
259-
}
260-
261261
var sessionData wauth.SessionData
262262
err = json.Unmarshal([]byte(sessionDataStr.(string)), &sessionData)
263263
if err != nil {
@@ -268,13 +268,84 @@ func WebauthnFinishLogin(c *gin.Context) {
268268
return
269269
}
270270

271-
_, err = webauthnInstance.FinishLogin(user, sessionData, c.Request)
272-
if err != nil {
273-
c.JSON(http.StatusBadRequest, gin.H{
274-
"message": "登录验证失败: " + err.Error(),
275-
"success": false,
276-
})
277-
return
271+
var user *model.User
272+
273+
if userIdStr != nil {
274+
// 有用户名的登录
275+
userId, err := strconv.Atoi(userIdStr.(string))
276+
if err != nil {
277+
c.JSON(http.StatusInternalServerError, gin.H{
278+
"message": "登陆失败",
279+
"success": false,
280+
})
281+
return
282+
}
283+
284+
user, err = model.GetUserById(userId, false)
285+
if err != nil {
286+
c.JSON(http.StatusNotFound, gin.H{
287+
"message": "登陆失败",
288+
"success": false,
289+
})
290+
return
291+
}
292+
293+
_, err = webauthnInstance.FinishLogin(user, sessionData, c.Request)
294+
if err != nil {
295+
c.JSON(http.StatusBadRequest, gin.H{
296+
"message": "登录验证失败: " + err.Error(),
297+
"success": false,
298+
})
299+
return
300+
}
301+
} else {
302+
// 无用户名登录 (Discoverable Login)
303+
var foundUser wauth.User
304+
_, err := webauthnInstance.FinishDiscoverableLogin(func(rawID, userHandle []byte) (wauth.User, error) {
305+
if len(userHandle) > 0 {
306+
userId, err := strconv.Atoi(string(userHandle))
307+
if err == nil {
308+
u, err := model.GetUserById(userId, false)
309+
if err == nil {
310+
foundUser = u
311+
return u, nil
312+
}
313+
}
314+
}
315+
// Fallback to lookup by credential ID
316+
u, err := model.GetUserByWebAuthnCredentialId(rawID)
317+
if err == nil {
318+
foundUser = u
319+
return u, nil
320+
}
321+
return nil, errors.New("user not found")
322+
}, sessionData, c.Request)
323+
324+
if err != nil {
325+
c.JSON(http.StatusBadRequest, gin.H{
326+
"message": "登录验证失败: " + err.Error(),
327+
"success": false,
328+
})
329+
return
330+
}
331+
332+
if foundUser == nil {
333+
c.JSON(http.StatusBadRequest, gin.H{
334+
"message": "无法找到对应的用户",
335+
"success": false,
336+
})
337+
return
338+
}
339+
340+
var ok bool
341+
user, ok = foundUser.(*model.User)
342+
if !ok {
343+
c.JSON(http.StatusInternalServerError, gin.H{
344+
"message": "用户类型断言失败",
345+
"success": false,
346+
})
347+
return
348+
}
278349
}
279350

280351
// 检查用户状态

model/user.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,3 +656,13 @@ func SaveWebAuthnCredential(userId int, credential *webauthn.Credential, alias s
656656
}
657657
return DB.Create(&webauthnCred).Error
658658
}
659+
660+
// GetUserByWebAuthnCredentialId 通过WebAuthn凭据ID获取用户
661+
func GetUserByWebAuthnCredentialId(credentialId []byte) (*User, error) {
662+
var cred WebAuthnCredential
663+
err := DB.Where("credential_id = ?", credentialId).First(&cred).Error
664+
if err != nil {
665+
return nil, err
666+
}
667+
return GetUserById(cred.UserId, false)
668+
}

router/api-router.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func SetApiRouter(router *gin.Engine) {
8080
selfRoute.GET("/invoice/detail", controller.GetUserInvoiceDetail)
8181
selfRoute.GET("/self", controller.GetSelf)
8282
selfRoute.PUT("/self", controller.UpdateSelf)
83+
selfRoute.POST("/unbind", controller.Unbind)
8384
// selfRoute.DELETE("/self", controller.DeleteSelf)
8485
selfRoute.GET("/token", controller.GenerateAccessToken)
8586
selfRoute.GET("/aff", controller.GetAffCode)

web/src/i18n/locales/en_US.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,9 @@
761761
"password": "Password",
762762
"passwordMinLength": "Password must be at least 8 characters long",
763763
"personalInfo": "Personal Information",
764+
"general": "General",
765+
"binding": "Binding",
766+
"webauthn": "WebAuthn",
764767
"resetToken": "Reset Access Token",
765768
"submit": "Submit",
766769
"telegramBot": "Telegram bot",
@@ -781,7 +784,23 @@
781784
"alias": "Alias:",
782785
"credentialId": "Credential ID:",
783786
"registerTime": "Register Time:",
784-
"delete": "Delete"
787+
"delete": "Delete",
788+
"noWebauthnCredentials": "No WebAuthn credentials found",
789+
"confirmPassword": "Confirm Password",
790+
"inputConfirmPasswordPlaceholder": "Please enter password again",
791+
"passwordsNotMatch": "Passwords do not match",
792+
"group": "Group",
793+
"rate": "Rate",
794+
"speed": "Speed",
795+
"bound": "Bound",
796+
"unbound": "Unbound",
797+
"bind": "Bind",
798+
"unbind": "Unbind",
799+
"change": "Change",
800+
"unbindSuccess": "Unbind successful",
801+
"unbindConfirm": "Confirm Unbind",
802+
"unbindWarning": "Are you sure you want to unbind? You will not be able to log in using this method after unbinding.",
803+
"cancel": "Cancel"
785804
},
786805
"redemption": "Redemption",
787806
"redemptionPage": {

web/src/i18n/locales/ja_JP.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,9 @@
749749
"password": "パスワード",
750750
"passwordMinLength": "パスワードは8文字以上でなければなりません",
751751
"personalInfo": "個人情報",
752+
"general": "一般",
753+
"binding": "連携",
754+
"webauthn": "WebAuthn",
752755
"resetToken": "アクセストークンのリセット",
753756
"submit": "送信",
754757
"telegramBot": "電報ボット",
@@ -769,7 +772,23 @@
769772
"alias": "エイリアス:",
770773
"credentialId": "認証情報ID:",
771774
"registerTime": "登録日時:",
772-
"delete": "削除"
775+
"delete": "削除",
776+
"noWebauthnCredentials": "WebAuthn 資格情報が見つかりません",
777+
"confirmPassword": "パスワードの確認",
778+
"inputConfirmPasswordPlaceholder": "パスワードを再入力してください",
779+
"passwordsNotMatch": "パスワードが一致しません",
780+
"group": "グループ",
781+
"rate": "レート",
782+
"speed": "速度",
783+
"bound": "連携済み",
784+
"unbound": "未連携",
785+
"bind": "連携",
786+
"unbind": "解除",
787+
"change": "変更",
788+
"unbindSuccess": "解除成功",
789+
"unbindConfirm": "解除確認",
790+
"unbindWarning": "本当に解除しますか?解除後はこの方法でログインできなくなります。",
791+
"cancel": "キャンセル"
773792
},
774793
"redemption": "引き換え",
775794
"redemptionPage": {

0 commit comments

Comments
 (0)