package auth import ( "database/sql" "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) var ErrInvalidCredentials = errors.New("invalid credentials") type Claims struct { VolunteerID int64 `json:"volunteer_id"` Role string `json:"role"` jwt.RegisteredClaims } type Service struct { db *sql.DB jwtSecret []byte } func NewService(db *sql.DB, secret string) *Service { return &Service{db: db, jwtSecret: []byte(secret)} } func (s *Service) Login(email, password string) (string, error) { var id int64 var hash, role string err := s.db.QueryRow( `SELECT id, password, role FROM volunteers WHERE email = ? AND active = 1`, email, ).Scan(&id, &hash, &role) if errors.Is(err, sql.ErrNoRows) { return "", ErrInvalidCredentials } if err != nil { return "", fmt.Errorf("query volunteer: %w", err) } if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil { return "", ErrInvalidCredentials } return s.issueToken(id, role) } func (s *Service) issueToken(volunteerID int64, role string) (string, error) { claims := Claims{ VolunteerID: volunteerID, Role: role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(s.jwtSecret) } func (s *Service) Parse(tokenStr string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method") } return s.jwtSecret, 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 } func HashPassword(password string) (string, error) { b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(b), err }