472bf8b732
Co-Authored-By: Codex <noreply@openai.com>
222 lines
6.1 KiB
Go
222 lines
6.1 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) {
|
|
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")
|
|
}
|
|
}
|