- Go API server with PostgreSQL + Redis - AI floor replacement (OpenRouter Gemini) - Product database (10 brands, 3539 products) - Recommendation engine, calculator, articles - Redis async queue + worker pool - Hot product caching, brand view tracking - JWT auth, favorites, projects - Docker deployment ready Co-Authored-By: Claude <noreply@anthropic.com>
121 lines
2.9 KiB
Go
121 lines
2.9 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
"floorvisualizer/internal/model"
|
|
"floorvisualizer/internal/repository"
|
|
)
|
|
|
|
type AuthService struct {
|
|
secret []byte
|
|
ttl time.Duration
|
|
}
|
|
|
|
func NewAuthService(secret string) *AuthService {
|
|
if secret == "" {
|
|
b := make([]byte, 32)
|
|
rand.Read(b)
|
|
secret = hex.EncodeToString(b)
|
|
}
|
|
return &AuthService{secret: []byte(secret), ttl: 72 * time.Hour}
|
|
}
|
|
|
|
func HashPassword(password string) string {
|
|
h := sha256.Sum256([]byte(password))
|
|
return hex.EncodeToString(h[:])
|
|
}
|
|
|
|
type Claims struct {
|
|
UserID string `json:"uid"`
|
|
Username string `json:"uname"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
func (s *AuthService) GenerateToken(user *model.User) (string, error) {
|
|
claims := Claims{
|
|
UserID: user.ID,
|
|
Username: user.Username,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.ttl)),
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
Issuer: "floorvisualizer",
|
|
},
|
|
}
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
return token.SignedString(s.secret)
|
|
}
|
|
|
|
func (s *AuthService) ParseToken(tokenStr string) (*Claims, error) {
|
|
token, err := jwt.ParseWithClaims(tokenStr, &Claims{},
|
|
func(t *jwt.Token) (interface{}, error) {
|
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, fmt.Errorf("unexpected signing method")
|
|
}
|
|
return s.secret, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
claims, ok := token.Claims.(*Claims)
|
|
if !ok || !token.Valid {
|
|
return nil, errors.New("invalid token")
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
type RegisterInput struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type LoginInput struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type AuthResponse struct {
|
|
Token string `json:"token"`
|
|
User model.User `json:"user"`
|
|
}
|
|
|
|
func (s *AuthService) Register(db *sql.DB, input RegisterInput) (*AuthResponse, error) {
|
|
if strings.TrimSpace(input.Username) == "" || strings.TrimSpace(input.Password) == "" {
|
|
return nil, errors.New("username and password are required")
|
|
}
|
|
user, err := repository.CreateUser(db, input.Username, HashPassword(input.Password), input.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("register: %w", err)
|
|
}
|
|
token, err := s.GenerateToken(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &AuthResponse{Token: token, User: *user}, nil
|
|
}
|
|
|
|
func (s *AuthService) Login(db *sql.DB, input LoginInput) (*AuthResponse, error) {
|
|
user, err := repository.GetUserByUsername(db, input.Username)
|
|
if err != nil {
|
|
return nil, errors.New("invalid username or password")
|
|
}
|
|
if HashPassword(input.Password) != user.PasswordHash {
|
|
return nil, errors.New("invalid username or password")
|
|
}
|
|
token, err := s.GenerateToken(user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &AuthResponse{Token: token, User: *user}, nil
|
|
}
|