fix: complete audit security and frontend gaps

This commit is contained in:
2026-07-18 18:02:29 +08:00
parent 79feb20688
commit b7a84d31ea
28 changed files with 576 additions and 88 deletions

View File

@@ -3,10 +3,13 @@ package auth
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
@@ -14,12 +17,14 @@ import (
)
type Service struct {
db *gorm.DB
secret string
db *gorm.DB
secret string
inviteTokenTTL time.Duration
sessionTTL time.Duration
}
func NewService(database *gorm.DB, secret string) *Service {
return &Service{db: database, secret: secret}
return &Service{db: database, secret: secret, inviteTokenTTL: 7 * 24 * time.Hour, sessionTTL: 24 * time.Hour}
}
func (s *Service) CreateInvite(adminID uint, email string) (string, error) {
@@ -27,20 +32,20 @@ func (s *Service) CreateInvite(adminID uint, email string) (string, error) {
if normalized == "" {
return "", errors.New("email is required")
}
return normalized + "." + s.sign(normalized), nil
return s.signToken(tokenPayload{
Type: "invite",
Subject: normalized,
IssuerID: adminID,
ExpiresAt: time.Now().Add(s.inviteTokenTTL).Unix(),
})
}
func (s *Service) RegisterWithInvite(token, email, displayName, password string) (*domain.User, error) {
separator := strings.LastIndex(token, ".")
if separator < 1 || separator == len(token)-1 {
return nil, errors.New("invalid invite token")
payload, err := s.verifyToken(token, "invite")
if err != nil {
return nil, err
}
inviteEmail := token[:separator]
signature := token[separator+1:]
if !hmac.Equal([]byte(signature), []byte(s.sign(inviteEmail))) {
return nil, errors.New("invalid invite signature")
}
if inviteEmail != strings.ToLower(strings.TrimSpace(email)) {
if payload.Subject != strings.ToLower(strings.TrimSpace(email)) {
return nil, errors.New("invite email mismatch")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
@@ -48,7 +53,7 @@ func (s *Service) RegisterWithInvite(token, email, displayName, password string)
return nil, err
}
user := &domain.User{
Email: inviteEmail,
Email: payload.Subject,
DisplayName: strings.TrimSpace(displayName),
PasswordHash: string(hash),
Role: "user",
@@ -64,7 +69,66 @@ func (s *Service) Login(email, password string) (string, error) {
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return "", errors.New("invalid credentials")
}
return fmt.Sprintf("user:%d", user.ID), nil
return s.signToken(tokenPayload{
Type: "session",
Subject: fmt.Sprintf("%d", user.ID),
ExpiresAt: time.Now().Add(s.sessionTTL).Unix(),
})
}
func (s *Service) VerifySession(token string) (uint, error) {
payload, err := s.verifyToken(token, "session")
if err != nil {
return 0, err
}
var userID uint
if _, err := fmt.Sscanf(payload.Subject, "%d", &userID); err != nil || userID == 0 {
return 0, errors.New("invalid session subject")
}
return userID, nil
}
type tokenPayload struct {
Type string `json:"type"`
Subject string `json:"subject"`
IssuerID uint `json:"issuer_id,omitempty"`
ExpiresAt int64 `json:"expires_at"`
}
func (s *Service) signToken(payload tokenPayload) (string, error) {
body, err := json.Marshal(payload)
if err != nil {
return "", err
}
encoded := base64.RawURLEncoding.EncodeToString(body)
return encoded + "." + s.sign(encoded), nil
}
func (s *Service) verifyToken(token string, expectedType string) (tokenPayload, error) {
separator := strings.LastIndex(token, ".")
if separator < 1 || separator == len(token)-1 {
return tokenPayload{}, errors.New("invalid token")
}
encoded := token[:separator]
signature := token[separator+1:]
if !hmac.Equal([]byte(signature), []byte(s.sign(encoded))) {
return tokenPayload{}, errors.New("invalid token signature")
}
body, err := base64.RawURLEncoding.DecodeString(encoded)
if err != nil {
return tokenPayload{}, errors.New("invalid token payload")
}
var payload tokenPayload
if err := json.Unmarshal(body, &payload); err != nil {
return tokenPayload{}, errors.New("invalid token payload")
}
if payload.Type != expectedType {
return tokenPayload{}, errors.New("invalid token type")
}
if payload.ExpiresAt <= time.Now().Unix() {
return tokenPayload{}, errors.New("token expired")
}
return payload, nil
}
func (s *Service) sign(value string) string {