Files
infinite-canvas/service/auth.go
T
HouYunFei d40b9b53c2 fix(ai): 修复AI代理请求和图片生成功能
- 添加详细的错误日志记录,包括请求读取、渠道选择、请求构建和响应处理的失败情况
- 统一AI接口请求失败返回消息为"AI 接口请求失败"
- 添加对上游响应状态码>=400的检查和错误处理
- 在JWT解析中验证签名方法有效性,防止无效登录状态
- 移除JWT密钥警告日志,改为运行时生成随机密钥
- 调整应用配置模态框中云端和本地选项的显示顺序
- 添加图片生成元数据记录生成类型、模型、尺寸、质量和参考图等信息
- 实现空图片节点直接生成图片而不保留空框的功能
- 完善图片重试逻辑,优先使用保存的元数据进行重试
- 添加参考图片丢失时的错误提示
- 修复图片节点样式圆角显示问题
- 更新AI代理路径为/api/v1/*保持OpenAI风格
- 添加系统设置页面的安全警告提示
2026-05-21 17:50:14 +08:00

222 lines
6.0 KiB
Go

package service
import (
"errors"
"log"
"strings"
"time"
"github.com/basketikun/infinite-canvas/config"
"github.com/basketikun/infinite-canvas/model"
"github.com/basketikun/infinite-canvas/repository"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
type TokenClaims struct {
UserID string `json:"userId"`
Username string `json:"username"`
Role model.UserRole `json:"role"`
jwt.RegisteredClaims
}
func EnsureDefaultAdmin() error {
if strings.TrimSpace(config.Cfg.AdminUsername) == "" || strings.TrimSpace(config.Cfg.AdminPassword) == "" {
return nil
}
WarnDefaultSecurityConfig()
hasAdmin, err := repository.HasAdmin()
if err != nil || hasAdmin {
return err
}
hash, err := hashPassword(config.Cfg.AdminPassword)
if err != nil {
return err
}
_, err = repository.SaveUser(model.User{
ID: newID("user"),
Username: strings.TrimSpace(config.Cfg.AdminUsername),
Password: hash,
Role: model.UserRoleAdmin,
CreatedAt: now(),
UpdatedAt: now(),
})
return err
}
func Register(username string, password string) (model.AuthSession, error) {
return model.AuthSession{}, errors.New("注册功能暂时关闭")
username = strings.TrimSpace(username)
if strings.ContainsAny(username, " \t\r\n") {
return model.AuthSession{}, errors.New("用户名不能包含空格")
}
if username == "" || password == "" {
return model.AuthSession{}, errors.New("用户名和密码不能为空")
}
if _, ok, err := repository.GetUserByUsername(username); err != nil || ok {
if err != nil {
return model.AuthSession{}, err
}
return model.AuthSession{}, errors.New("用户名已存在")
}
hash, err := hashPassword(password)
if err != nil {
return model.AuthSession{}, err
}
user, err := repository.SaveUser(model.User{
ID: newID("user"),
Username: username,
Password: hash,
Role: model.UserRoleUser,
CreatedAt: now(),
UpdatedAt: now(),
})
if err != nil {
return model.AuthSession{}, err
}
return newSession(user)
}
func Login(username string, password string) (model.AuthSession, error) {
user, ok, err := repository.GetUserByUsername(strings.TrimSpace(username))
if err != nil {
return model.AuthSession{}, err
}
if !ok || bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) != nil {
return model.AuthSession{}, errors.New("用户名或密码错误")
}
return newSession(user)
}
func ParseToken(tokenText string) (TokenClaims, error) {
claims := TokenClaims{}
token, err := jwt.ParseWithClaims(tokenText, &claims, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("登录状态无效")
}
return []byte(config.Cfg.JWTSecret), nil
})
if err != nil || !token.Valid {
return TokenClaims{}, errors.New("登录状态无效")
}
return claims, nil
}
func CurrentAuthUser(tokenText string) (model.AuthUser, bool) {
claims, err := ParseToken(tokenText)
if err != nil {
return model.AuthUser{}, false
}
user, ok, err := repository.GetUserByID(claims.UserID)
if err != nil || !ok {
return model.AuthUser{}, false
}
return model.PublicUser(user), true
}
func ListUsers(q model.Query) (model.UserList, error) {
users, total, err := repository.ListUsers(q)
if err != nil {
return model.UserList{}, err
}
for i := range users {
users[i].Password = ""
}
return model.UserList{Items: users, Total: int(total)}, nil
}
func SaveUser(user model.User, password string) (model.User, error) {
user.Username = strings.TrimSpace(user.Username)
if strings.ContainsAny(user.Username, " \t\r\n") {
return user, errors.New("用户名不能包含空格")
}
if user.Username == "" {
return user, errors.New("用户名不能为空")
}
if user.Role == "" || user.Role == model.UserRoleGuest {
user.Role = model.UserRoleUser
}
if saved, ok, err := repository.GetUserByUsername(user.Username); err != nil {
return user, err
} else if ok && saved.ID != user.ID {
return user, errors.New("用户名已存在")
}
if user.ID == "" {
user.ID = newID("user")
user.CreatedAt = now()
} else if saved, ok, err := repository.GetUserByID(user.ID); err != nil {
return user, err
} else if ok {
user.CreatedAt = saved.CreatedAt
user.Password = saved.Password
}
if password != "" {
hash, err := hashPassword(password)
if err != nil {
return user, err
}
user.Password = hash
}
if user.Password == "" {
return user, errors.New("密码不能为空")
}
user.UpdatedAt = now()
user, err := repository.SaveUser(user)
user.Password = ""
return user, err
}
func DeleteUser(id string) error {
return repository.DeleteUser(id)
}
func GuestUser() model.AuthUser {
return model.AuthUser{ID: "", Username: "guest", Role: model.UserRoleGuest}
}
func newSession(user model.User) (model.AuthSession, error) {
token, err := newToken(user)
if err != nil {
return model.AuthSession{}, err
}
return model.AuthSession{Token: token, User: model.PublicUser(user)}, nil
}
func newToken(user model.User) (string, error) {
expireHours := config.Cfg.JWTExpireHours
if expireHours <= 0 {
expireHours = 168
}
claims := TokenClaims{
UserID: user.ID,
Username: user.Username,
Role: user.Role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Subject: user.ID,
},
}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(config.Cfg.JWTSecret))
}
func hashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(hash), err
}
func now() string {
return time.Now().Format(time.RFC3339)
}
func newID(prefix string) string {
return prefix + "-" + uuid.NewString()
}
func WarnDefaultSecurityConfig() {
if config.Cfg.AdminUsername == "admin" && config.Cfg.AdminPassword == "infinite-canvas" {
log.Println("WARNING: using default admin credentials, please set ADMIN_USERNAME and ADMIN_PASSWORD to safer values before deployment")
}
}