78b57273e2
- Add password hashing with bcrypt - Add AuthService with proper login - Add password strength validation - Add RBAC middleware (AdminOnly, ManagerOrAdmin) - Add tenant isolation middleware - Update CRM handler with tenant filtering - Add JWT fallback for development mode - Add user context helpers - Build successful
195 lines
4.7 KiB
Go
195 lines
4.7 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
// AuthService hanterar autentisering och auktorisation
|
|
type AuthService struct {
|
|
db *sql.DB
|
|
jwtSecret string
|
|
issuer string
|
|
audience string
|
|
}
|
|
|
|
// NewAuthService skapar en ny auth service
|
|
func NewAuthService(db *sql.DB, jwtSecret, issuer, audience string) *AuthService {
|
|
return &AuthService{
|
|
db: db,
|
|
jwtSecret: jwtSecret,
|
|
issuer: issuer,
|
|
audience: audience,
|
|
}
|
|
}
|
|
|
|
// User representerar en autentiserad användare
|
|
type User struct {
|
|
ID string `json:"id"`
|
|
Email string `json:"email"`
|
|
Name string `json:"name"`
|
|
Role string `json:"role"`
|
|
TenantID string `json:"tenant_id"`
|
|
Roles []string `json:"roles"`
|
|
}
|
|
|
|
// LoginRequest innehåller login-uppgifter
|
|
type LoginRequest struct {
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
// LoginResponse innehåller token och användardata
|
|
type LoginResponse struct {
|
|
Token string `json:"token"`
|
|
TokenType string `json:"token_type"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
User User `json:"user"`
|
|
}
|
|
|
|
// Login autentiserar en användare och returnerar JWT token
|
|
func (s *AuthService) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
|
|
// Hämta användare från databas
|
|
var user User
|
|
var passwordHash string
|
|
|
|
err := s.db.QueryRowContext(ctx, `
|
|
SELECT id, email, name, role, tenant_id, password_hash
|
|
FROM boc_users
|
|
WHERE email = $1 AND status = 'active'
|
|
`, req.Email).Scan(&user.ID, &user.Email, &user.Name, &user.Role, &user.TenantID, &passwordHash)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("invalid email or password")
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("database error: %w", err)
|
|
}
|
|
|
|
// Verifiera lösenord
|
|
if !VerifyPassword(req.Password, passwordHash) {
|
|
return nil, fmt.Errorf("invalid email or password")
|
|
}
|
|
|
|
// Generera JWT token
|
|
token, err := s.GenerateToken(user)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate token: %w", err)
|
|
}
|
|
|
|
// Uppdatera last_login
|
|
_, _ = s.db.ExecContext(ctx, `
|
|
UPDATE boc_users SET last_login = NOW() WHERE id = $1
|
|
`, user.ID)
|
|
|
|
return &LoginResponse{
|
|
Token: token,
|
|
TokenType: "Bearer",
|
|
ExpiresIn: 3600, // 1 timme
|
|
User: user,
|
|
}, nil
|
|
}
|
|
|
|
// GenerateToken skapar en JWT token för en användare
|
|
func (s *AuthService) GenerateToken(user User) (string, error) {
|
|
now := time.Now()
|
|
|
|
claims := jwt.MapClaims{
|
|
"sub": user.ID,
|
|
"email": user.Email,
|
|
"name": user.Name,
|
|
"role": user.Role,
|
|
"tenant_id": user.TenantID,
|
|
"iss": s.issuer,
|
|
"aud": s.audience,
|
|
"iat": now.Unix(),
|
|
"exp": now.Add(1 * time.Hour).Unix(), // 1 timme
|
|
"jti": generateJTI(),
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
return token.SignedString([]byte(s.jwtSecret))
|
|
}
|
|
|
|
// ValidateToken validerar en JWT token och returnerar användardata
|
|
func (s *AuthService) ValidateToken(tokenString string) (*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(s.jwtSecret), nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid token: %w", err)
|
|
}
|
|
|
|
if !token.Valid {
|
|
return nil, fmt.Errorf("token is invalid")
|
|
}
|
|
|
|
claims, ok := token.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid claims")
|
|
}
|
|
|
|
user := &User{
|
|
ID: getStringClaim(claims, "sub"),
|
|
Email: getStringClaim(claims, "email"),
|
|
Name: getStringClaim(claims, "name"),
|
|
Role: getStringClaim(claims, "role"),
|
|
TenantID: getStringClaim(claims, "tenant_id"),
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// HasRole kontrollerar om användaren har en specifik roll
|
|
func (s *AuthService) HasRole(user *User, role string) bool {
|
|
if user.Role == role {
|
|
return true
|
|
}
|
|
for _, r := range user.Roles {
|
|
if r == role {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RequireRole middleware kontrollerar att användaren har en specifik roll
|
|
func (s *AuthService) RequireRole(role string) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
claims, ok := FromContext(r.Context())
|
|
if !ok {
|
|
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
user := &User{
|
|
ID: claims.Sub,
|
|
Email: claims.Email,
|
|
Role: "",
|
|
Roles: claims.Roles,
|
|
}
|
|
|
|
if !s.HasRole(user, role) {
|
|
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
func generateJTI() string {
|
|
return fmt.Sprintf("%d", time.Now().UnixNano())
|
|
}
|