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) { 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") } if config.Cfg.JWTSecret == "infinite-canvas" { log.Println("WARNING: using default JWT_SECRET, please set a long random value before deployment") } }