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()) }