+221
@@ -0,0 +1,221 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user