package services
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"strings"
"sync"
"time"
"app/models"
)
const (
TokenExpiry = 24 * time.Hour
MaxSessions = 1000
MinPasswordLength = 6
)
type AuthToken string
type Session struct {
Token AuthToken
UserID uint64
CreatedAt time.Time
ExpiresAt time.Time
}
func (s *Session) IsExpired() bool {
return time.Now().After(s.ExpiresAt)
}
type AuthService struct {
db *DatabaseConnection
sessions map[AuthToken]*Session
users map[string]*models.User mutex sync.RWMutex
tokenCounter uint64
}
func NewAuthService(db *DatabaseConnection) *AuthService {
return &AuthService{
db: db,
sessions: make(map[AuthToken]*Session),
users: make(map[string]*models.User),
mutex: sync.RWMutex{},
}
}
func (a *AuthService) RegisterUser(user *models.User) error {
a.mutex.Lock()
defer a.mutex.Unlock()
if err := user.Validate(); err != nil {
return fmt.Errorf("user validation failed: %w", err)
}
if _, exists := a.users[user.Email()]; exists {
return NewAuthError(ErrDuplicateEmail, "email already registered")
}
a.users[user.Email()] = user
if err := a.db.Execute("INSERT INTO users (name, email, role) VALUES (?, ?, ?)",
[]interface{}{user.Name(), user.Email(), user.Role().String()}); err != nil {
return fmt.Errorf("database error: %w", err)
}
return nil
}
func (a *AuthService) Authenticate(ctx context.Context, email, password string) (AuthToken, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}
a.mutex.RLock()
user, exists := a.users[email]
a.mutex.RUnlock()
if !exists {
return "", NewAuthError(ErrUserNotFound, "user not found")
}
if len(password) < MinPasswordLength {
return "", NewAuthError(ErrInvalidCredentials, "invalid credentials")
}
token := a.generateToken(user)
session := &Session{
Token: token,
UserID: user.ID,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(TokenExpiry),
}
a.mutex.Lock()
a.sessions[token] = session
a.mutex.Unlock()
user.UpdateLastLogin()
return token, nil
}
func (a *AuthService) ValidateSession(token AuthToken) (*models.User, error) {
a.mutex.RLock()
session, exists := a.sessions[token]
a.mutex.RUnlock()
if !exists {
return nil, NewAuthError(ErrInvalidToken, "invalid token")
}
if session.IsExpired() {
a.mutex.Lock()
delete(a.sessions, token)
a.mutex.Unlock()
return nil, NewAuthError(ErrExpiredToken, "token expired")
}
a.mutex.RLock()
defer a.mutex.RUnlock()
for _, user := range a.users {
if user.ID == session.UserID {
return user, nil
}
}
return nil, NewAuthError(ErrUserNotFound, "user not found for session")
}
func (a *AuthService) Logout(token AuthToken) error {
a.mutex.Lock()
defer a.mutex.Unlock()
if _, exists := a.sessions[token]; !exists {
return NewAuthError(ErrInvalidToken, "invalid token")
}
delete(a.sessions, token)
return nil
}
func (a *AuthService) GetSessionCount() int {
a.mutex.RLock()
defer a.mutex.RUnlock()
count := 0
for _, session := range a.sessions {
if !session.IsExpired() {
count++
}
}
return count
}
func (a *AuthService) CleanupExpiredSessions() int {
a.mutex.Lock()
defer a.mutex.Unlock()
removed := 0
for token, session := range a.sessions {
if session.IsExpired() {
delete(a.sessions, token)
removed++
}
}
return removed
}
func (a *AuthService) getUserByEmail(email string) *models.User {
a.mutex.RLock()
defer a.mutex.RUnlock()
return a.users[email]
}
func (a *AuthService) generateToken(user *models.User) AuthToken {
a.tokenCounter++
data := fmt.Sprintf("%d-%d-%d", user.ID, a.tokenCounter, time.Now().UnixNano())
hash := sha256.Sum256([]byte(data))
return AuthToken(fmt.Sprintf("%x", hash[:16])) }
func HashPassword(password string) string {
hash := sha256.Sum256([]byte(password + "salt"))
return fmt.Sprintf("%x", hash)
}
func VerifyPassword(password, hash string) bool {
return HashPassword(password) == hash
}
func ParseAuthError(errorMsg string) (*AuthError, error) {
errorMsg = strings.ToLower(errorMsg)
switch {
case strings.Contains(errorMsg, "not found"):
return NewAuthError(ErrUserNotFound, errorMsg), nil
case strings.Contains(errorMsg, "duplicate"):
return NewAuthError(ErrDuplicateEmail, errorMsg), nil
case strings.Contains(errorMsg, "invalid"):
return NewAuthError(ErrInvalidCredentials, errorMsg), nil
default:
return nil, errors.New("cannot parse error message")
}
}
type AuthErrorCode int
const (
ErrUserNotFound AuthErrorCode = iota
ErrDuplicateEmail
ErrInvalidCredentials
ErrInvalidToken
ErrExpiredToken
ErrTooManySessions
)
type AuthError struct {
Code AuthErrorCode
Message string
}
func NewAuthError(code AuthErrorCode, message string) *AuthError {
return &AuthError{
Code: code,
Message: message,
}
}
func (e *AuthError) Error() string {
return fmt.Sprintf("auth error [%d]: %s", int(e.Code), e.Message)
}
func (e *AuthError) Is(target error) bool {
if other, ok := target.(*AuthError); ok {
return e.Code == other.Code
}
return false
}
func (e *AuthError) FromUserError(userErr *models.UserError) *AuthError {
switch userErr.Code {
case 3: return NewAuthError(ErrUserNotFound, userErr.Message)
case 4: return NewAuthError(ErrDuplicateEmail, userErr.Message)
default:
return NewAuthError(ErrInvalidCredentials, userErr.Message)
}
}
func init() {
fmt.Println("[INIT] Auth service package initialized")
}