4ddc468fea
- Implemented GetUserByTenantID method in userRepository to retrieve the first user associated with a given tenant ID. - Added corresponding GetUserByTenantID method in userService to facilitate user retrieval by tenant ID. - Updated UserService and UserRepository interfaces to include the new method, ensuring consistency across the application. - Enhanced auth middleware to store user information in context based on tenant ID, improving user management. These changes enhance user retrieval capabilities by allowing access to user data based on tenant associations, improving overall application functionality.
442 lines
12 KiB
Go
442 lines
12 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/Tencent/WeKnora/internal/logger"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
secutils "github.com/Tencent/WeKnora/internal/utils"
|
|
)
|
|
|
|
var (
|
|
jwtSecretOnce sync.Once
|
|
jwtSecret string
|
|
)
|
|
|
|
// getJwtSecret retrieves the JWT secret from the environment, falling back to a securely generated random secret.
|
|
func getJwtSecret() string {
|
|
jwtSecretOnce.Do(func() {
|
|
if envSecret := strings.TrimSpace(os.Getenv("JWT_SECRET")); envSecret != "" {
|
|
jwtSecret = envSecret
|
|
return
|
|
}
|
|
|
|
randomBytes := make([]byte, 32)
|
|
if _, err := rand.Read(randomBytes); err != nil {
|
|
panic(fmt.Sprintf("failed to generate JWT secret: %v", err))
|
|
}
|
|
jwtSecret = base64.StdEncoding.EncodeToString(randomBytes)
|
|
})
|
|
|
|
return jwtSecret
|
|
}
|
|
|
|
// userService implements the UserService interface
|
|
type userService struct {
|
|
userRepo interfaces.UserRepository
|
|
tokenRepo interfaces.AuthTokenRepository
|
|
tenantService interfaces.TenantService
|
|
}
|
|
|
|
// NewUserService creates a new user service instance
|
|
func NewUserService(
|
|
userRepo interfaces.UserRepository,
|
|
tokenRepo interfaces.AuthTokenRepository,
|
|
tenantService interfaces.TenantService,
|
|
) interfaces.UserService {
|
|
return &userService{
|
|
userRepo: userRepo,
|
|
tokenRepo: tokenRepo,
|
|
tenantService: tenantService,
|
|
}
|
|
}
|
|
|
|
// Register creates a new user account
|
|
func (s *userService) Register(ctx context.Context, req *types.RegisterRequest) (*types.User, error) {
|
|
logger.Info(ctx, "Start user registration")
|
|
|
|
// Validate input
|
|
if req.Username == "" || req.Email == "" || req.Password == "" {
|
|
return nil, errors.New("username, email and password are required")
|
|
}
|
|
|
|
// Check if user already exists
|
|
existingUser, _ := s.userRepo.GetUserByEmail(ctx, req.Email)
|
|
if existingUser != nil {
|
|
return nil, errors.New("user with this email already exists")
|
|
}
|
|
|
|
existingUser, _ = s.userRepo.GetUserByUsername(ctx, req.Username)
|
|
if existingUser != nil {
|
|
return nil, errors.New("user with this username already exists")
|
|
}
|
|
|
|
// Hash password
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to hash password: %v", err)
|
|
return nil, errors.New("failed to process password")
|
|
}
|
|
|
|
// Create default tenant for the user
|
|
// Note: RetrieverEngines is left empty - system will use defaults from RETRIEVE_DRIVER env
|
|
tenant := &types.Tenant{
|
|
Name: fmt.Sprintf("%s's Workspace", secutils.SanitizeForLog(req.Username)),
|
|
Description: "Default workspace",
|
|
Status: "active",
|
|
}
|
|
|
|
createdTenant, err := s.tenantService.CreateTenant(ctx, tenant)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to create tenant")
|
|
return nil, errors.New("failed to create workspace")
|
|
}
|
|
|
|
// Create user
|
|
user := &types.User{
|
|
ID: uuid.New().String(),
|
|
Username: req.Username,
|
|
Email: req.Email,
|
|
PasswordHash: string(hashedPassword),
|
|
TenantID: createdTenant.ID,
|
|
IsActive: true,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
err = s.userRepo.CreateUser(ctx, user)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to create user: %v", err)
|
|
return nil, errors.New("failed to create user")
|
|
}
|
|
|
|
logger.Info(ctx, "User registered successfully")
|
|
return user, nil
|
|
}
|
|
|
|
// Login authenticates a user and returns tokens
|
|
func (s *userService) Login(ctx context.Context, req *types.LoginRequest) (*types.LoginResponse, error) {
|
|
logger.Info(ctx, "Start user login")
|
|
// Get user by email
|
|
user, err := s.userRepo.GetUserByEmail(ctx, req.Email)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to get user by email: %v", err)
|
|
return &types.LoginResponse{
|
|
Success: false,
|
|
Message: "Invalid email or password",
|
|
}, nil
|
|
}
|
|
if user == nil {
|
|
logger.Warn(ctx, "User not found for email")
|
|
return &types.LoginResponse{
|
|
Success: false,
|
|
Message: "Invalid email or password",
|
|
}, nil
|
|
}
|
|
|
|
// Check if user is active
|
|
if !user.IsActive {
|
|
logger.Warn(ctx, "User account is disabled")
|
|
return &types.LoginResponse{
|
|
Success: false,
|
|
Message: "Account is disabled",
|
|
}, nil
|
|
}
|
|
|
|
// Verify password
|
|
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
|
|
if err != nil {
|
|
logger.Warn(ctx, "Password verification failed")
|
|
return &types.LoginResponse{
|
|
Success: false,
|
|
Message: "Invalid email or password",
|
|
}, nil
|
|
}
|
|
logger.Info(ctx, "Password verification successful")
|
|
|
|
// Generate tokens
|
|
logger.Info(ctx, "Generating tokens")
|
|
accessToken, refreshToken, err := s.GenerateTokens(ctx, user)
|
|
if err != nil {
|
|
logger.Errorf(ctx, "Failed to generate tokens: %v", err)
|
|
return &types.LoginResponse{
|
|
Success: false,
|
|
Message: "Login failed",
|
|
}, nil
|
|
}
|
|
logger.Info(ctx, "Tokens generated successfully")
|
|
|
|
// Get tenant information
|
|
tenant, err := s.tenantService.GetTenantByID(ctx, user.TenantID)
|
|
if err != nil {
|
|
logger.Warn(ctx, "Failed to get tenant info")
|
|
} else {
|
|
logger.Info(ctx, "Tenant information retrieved successfully")
|
|
}
|
|
|
|
logger.Info(ctx, "User logged in successfully")
|
|
return &types.LoginResponse{
|
|
Success: true,
|
|
Message: "Login successful",
|
|
User: user,
|
|
Tenant: tenant,
|
|
Token: accessToken,
|
|
RefreshToken: refreshToken,
|
|
}, nil
|
|
}
|
|
|
|
// GetUserByID gets a user by ID
|
|
func (s *userService) GetUserByID(ctx context.Context, id string) (*types.User, error) {
|
|
return s.userRepo.GetUserByID(ctx, id)
|
|
}
|
|
|
|
// GetUserByEmail gets a user by email
|
|
func (s *userService) GetUserByEmail(ctx context.Context, email string) (*types.User, error) {
|
|
return s.userRepo.GetUserByEmail(ctx, email)
|
|
}
|
|
|
|
// GetUserByUsername gets a user by username
|
|
func (s *userService) GetUserByUsername(ctx context.Context, username string) (*types.User, error) {
|
|
return s.userRepo.GetUserByUsername(ctx, username)
|
|
}
|
|
|
|
// GetUserByTenantID gets the first user (owner) of a tenant
|
|
func (s *userService) GetUserByTenantID(ctx context.Context, tenantID uint64) (*types.User, error) {
|
|
return s.userRepo.GetUserByTenantID(ctx, tenantID)
|
|
}
|
|
|
|
// UpdateUser updates user information
|
|
func (s *userService) UpdateUser(ctx context.Context, user *types.User) error {
|
|
user.UpdatedAt = time.Now()
|
|
return s.userRepo.UpdateUser(ctx, user)
|
|
}
|
|
|
|
// DeleteUser deletes a user
|
|
func (s *userService) DeleteUser(ctx context.Context, id string) error {
|
|
return s.userRepo.DeleteUser(ctx, id)
|
|
}
|
|
|
|
// ChangePassword changes user password
|
|
func (s *userService) ChangePassword(ctx context.Context, userID string, oldPassword, newPassword string) error {
|
|
user, err := s.userRepo.GetUserByID(ctx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify old password
|
|
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(oldPassword))
|
|
if err != nil {
|
|
return errors.New("invalid old password")
|
|
}
|
|
|
|
// Hash new password
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
user.PasswordHash = string(hashedPassword)
|
|
user.UpdatedAt = time.Now()
|
|
|
|
return s.userRepo.UpdateUser(ctx, user)
|
|
}
|
|
|
|
// ValidatePassword validates user password
|
|
func (s *userService) ValidatePassword(ctx context.Context, userID string, password string) error {
|
|
user, err := s.userRepo.GetUserByID(ctx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
|
|
}
|
|
|
|
// GenerateTokens generates access and refresh tokens for user
|
|
func (s *userService) GenerateTokens(
|
|
ctx context.Context,
|
|
user *types.User,
|
|
) (accessToken, refreshToken string, err error) {
|
|
// Generate access token (expires in 24 hours)
|
|
accessClaims := jwt.MapClaims{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
"tenant_id": user.TenantID,
|
|
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
|
"iat": time.Now().Unix(),
|
|
"type": "access",
|
|
}
|
|
|
|
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
|
accessToken, err = accessTokenObj.SignedString([]byte(getJwtSecret()))
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
// Generate refresh token (expires in 7 days)
|
|
refreshClaims := jwt.MapClaims{
|
|
"user_id": user.ID,
|
|
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
|
|
"iat": time.Now().Unix(),
|
|
"type": "refresh",
|
|
}
|
|
|
|
refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
|
|
refreshToken, err = refreshTokenObj.SignedString([]byte(getJwtSecret()))
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
// Store tokens in database
|
|
accessTokenRecord := &types.AuthToken{
|
|
ID: uuid.New().String(),
|
|
UserID: user.ID,
|
|
Token: accessToken,
|
|
TokenType: "access_token",
|
|
ExpiresAt: time.Now().Add(24 * time.Hour),
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
refreshTokenRecord := &types.AuthToken{
|
|
ID: uuid.New().String(),
|
|
UserID: user.ID,
|
|
Token: refreshToken,
|
|
TokenType: "refresh_token",
|
|
ExpiresAt: time.Now().Add(7 * 24 * time.Hour),
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
_ = s.tokenRepo.CreateToken(ctx, accessTokenRecord)
|
|
_ = s.tokenRepo.CreateToken(ctx, refreshTokenRecord)
|
|
|
|
return accessToken, refreshToken, nil
|
|
}
|
|
|
|
// ValidateToken validates an access token
|
|
func (s *userService) ValidateToken(ctx context.Context, tokenString string) (*types.User, error) {
|
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
|
}
|
|
return []byte(getJwtSecret()), nil
|
|
})
|
|
|
|
if err != nil || !token.Valid {
|
|
return nil, errors.New("invalid token")
|
|
}
|
|
|
|
claims, ok := token.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return nil, errors.New("invalid token claims")
|
|
}
|
|
|
|
userID, ok := claims["user_id"].(string)
|
|
if !ok {
|
|
return nil, errors.New("invalid user ID in token")
|
|
}
|
|
|
|
// Check if token is revoked
|
|
tokenRecord, err := s.tokenRepo.GetTokenByValue(ctx, tokenString)
|
|
if err != nil || tokenRecord == nil || tokenRecord.IsRevoked {
|
|
return nil, errors.New("token is revoked")
|
|
}
|
|
|
|
return s.userRepo.GetUserByID(ctx, userID)
|
|
}
|
|
|
|
// RefreshToken refreshes access token using refresh token
|
|
func (s *userService) RefreshToken(
|
|
ctx context.Context,
|
|
refreshTokenString string,
|
|
) (accessToken, newRefreshToken string, err error) {
|
|
token, err := jwt.Parse(refreshTokenString, func(token *jwt.Token) (interface{}, error) {
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
|
}
|
|
return []byte(getJwtSecret()), nil
|
|
})
|
|
|
|
if err != nil || !token.Valid {
|
|
return "", "", errors.New("invalid refresh token")
|
|
}
|
|
|
|
claims, ok := token.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return "", "", errors.New("invalid token claims")
|
|
}
|
|
|
|
tokenType, ok := claims["type"].(string)
|
|
if !ok || tokenType != "refresh" {
|
|
return "", "", errors.New("not a refresh token")
|
|
}
|
|
|
|
userID, ok := claims["user_id"].(string)
|
|
if !ok {
|
|
return "", "", errors.New("invalid user ID in token")
|
|
}
|
|
|
|
// Check if token is revoked
|
|
tokenRecord, err := s.tokenRepo.GetTokenByValue(ctx, refreshTokenString)
|
|
if err != nil || tokenRecord == nil || tokenRecord.IsRevoked {
|
|
return "", "", errors.New("refresh token is revoked")
|
|
}
|
|
|
|
// Get user
|
|
user, err := s.userRepo.GetUserByID(ctx, userID)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
// Revoke old refresh token
|
|
tokenRecord.IsRevoked = true
|
|
_ = s.tokenRepo.UpdateToken(ctx, tokenRecord)
|
|
|
|
// Generate new tokens
|
|
return s.GenerateTokens(ctx, user)
|
|
}
|
|
|
|
// RevokeToken revokes a token
|
|
func (s *userService) RevokeToken(ctx context.Context, tokenString string) error {
|
|
tokenRecord, err := s.tokenRepo.GetTokenByValue(ctx, tokenString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tokenRecord.IsRevoked = true
|
|
tokenRecord.UpdatedAt = time.Now()
|
|
|
|
return s.tokenRepo.UpdateToken(ctx, tokenRecord)
|
|
}
|
|
|
|
// GetCurrentUser gets current user from context
|
|
func (s *userService) GetCurrentUser(ctx context.Context) (*types.User, error) {
|
|
user, ok := ctx.Value(types.UserContextKey).(*types.User)
|
|
if !ok {
|
|
return nil, errors.New("user not found in context")
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// SearchUsers searches users by username or email
|
|
func (s *userService) SearchUsers(ctx context.Context, query string, limit int) ([]*types.User, error) {
|
|
if query == "" {
|
|
return []*types.User{}, nil
|
|
}
|
|
return s.userRepo.SearchUsers(ctx, query, limit)
|
|
}
|