Files
KanzlAI-mGMT/backend/internal/auth/middleware.go
m 642877ae54 feat: document templates with auto-fill from case data (P1)
- Database: kanzlai.document_templates table with RLS policies
- Seed: 4 system templates (Klageerwiderung UPC, Berufungsschrift,
  Mandatsbestätigung, Kostenrechnung)
- Backend: TemplateService (CRUD + render), TemplateHandler with
  endpoints: GET/POST /api/templates, GET/PUT/DELETE /api/templates/{id},
  POST /api/templates/{id}/render?case_id=X
- Template variables: case.*, party.*, tenant.*, user.*, date.*, deadline.*
- Frontend: /vorlagen page with category filters, template detail/editor,
  render flow (select case -> preview -> copy/download), variable toolbar
- Quick action: "Schriftsatz erstellen" button on case detail page
- Also: resolved merge conflicts between audit-trail and role-based branches,
  added missing Notification/AuditLog types to frontend
2026-03-30 11:26:25 +02:00

103 lines
2.4 KiB
Go

package auth
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
type Middleware struct {
jwtSecret []byte
db *sqlx.DB
}
func NewMiddleware(jwtSecret string, db *sqlx.DB) *Middleware {
return &Middleware{jwtSecret: []byte(jwtSecret), db: db}
}
func (m *Middleware) RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := extractBearerToken(r)
if token == "" {
http.Error(w, `{"error":"missing authorization token"}`, http.StatusUnauthorized)
return
}
userID, err := m.verifyJWT(token)
if err != nil {
http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized)
return
}
ctx := ContextWithUserID(r.Context(), userID)
// Capture IP and user-agent for audit logging
ip := r.Header.Get("X-Forwarded-For")
if ip == "" {
ip = r.RemoteAddr
}
ctx = ContextWithRequestInfo(ctx, ip, r.UserAgent())
// Tenant resolution is handled by TenantResolver middleware for scoped routes.
// Tenant management routes handle their own access control.
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (m *Middleware) verifyJWT(tokenStr string) (uuid.UUID, error) {
parsedToken, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return m.jwtSecret, nil
})
if err != nil {
return uuid.Nil, fmt.Errorf("parsing JWT: %w", err)
}
if !parsedToken.Valid {
return uuid.Nil, fmt.Errorf("invalid JWT token")
}
claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok {
return uuid.Nil, fmt.Errorf("extracting JWT claims")
}
if exp, ok := claims["exp"].(float64); ok {
if time.Now().Unix() > int64(exp) {
return uuid.Nil, fmt.Errorf("JWT token has expired")
}
}
sub, ok := claims["sub"].(string)
if !ok {
return uuid.Nil, fmt.Errorf("missing sub claim in JWT")
}
userID, err := uuid.Parse(sub)
if err != nil {
return uuid.Nil, fmt.Errorf("invalid user ID format: %w", err)
}
return userID, nil
}
func extractBearerToken(r *http.Request) string {
auth := r.Header.Get("Authorization")
if auth == "" {
return ""
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
return ""
}
return parts[1]
}