package models import ( "bytes" "encoding/json" "fmt" "strconv" "time" "github.com/golang-jwt/jwt/v5" ) type AuthorizationRequest struct { UsersID string `json:"users_id"` Resource string `json:"resource"` Action string `json:"action"` } type AuthorizationResponse struct { Allowed bool `json:"allowed"` Reason string `json:"reason,omitempty"` } type ProjectClaim struct { ProjectID int `json:"project_id,omitempty"` Alias string `json:"alias,omitempty"` RoleID RoleIDs `json:"role_id,omitempty"` OfficeID int `json:"office_id,omitempty"` } func (p *ProjectClaim) UnmarshalJSON(data []byte) error { type Alias ProjectClaim aux := &struct { RoleIDs RoleIDs `json:"role_ids"` *Alias }{ Alias: (*Alias)(p), } if err := json.Unmarshal(data, &aux); err != nil { return err } if len(p.RoleID) == 0 && len(aux.RoleIDs) > 0 { p.RoleID = aux.RoleIDs } return nil } // RoleIDs represents one or more role IDs. // It is defined as a custom type so we can implement flexible JSON unmarshalling // that accepts a single string ("1"), a single number (1), or an array ([1,2,...]). type RoleIDs []int // UnmarshalJSON allows RoleIDs to be populated from different JSON shapes: // string: "1" -> [1] // number: 1 -> [1] // array: [1] or [1,2,...] -> [1] or [1,2,...] func (r *RoleIDs) UnmarshalJSON(data []byte) error { // Handle null or empty trimmed := bytes.TrimSpace(data) if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) { *r = nil return nil } switch trimmed[0] { case '"': // String value, e.g. "1" var s string if err := json.Unmarshal(trimmed, &s); err != nil { return err } if s == "" { *r = nil return nil } v, err := strconv.Atoi(s) if err != nil { return err } *r = RoleIDs{v} return nil case '[': // Standard JSON array of ints var arr []int if err := json.Unmarshal(trimmed, &arr); err != nil { return err } *r = RoleIDs(arr) return nil default: // Try to decode as a single number var v int if err := json.Unmarshal(trimmed, &v); err == nil { *r = RoleIDs{v} return nil } return fmt.Errorf("unsupported JSON for role_id: %s", string(trimmed)) } } type Claims struct { UsersID string `json:"users_id,omitempty"` EmailAddress string `json:"email_address,omitempty"` RoleID RoleIDs `json:"role_id"` AdditionalRoleID RoleIDs `json:"additional_role_id,omitempty"` Projects []ProjectClaim `json:"projects,omitempty"` jwt.RegisteredClaims } // UnmarshalJSON handles both "user_id" and "users_id" field names in JWT claims func (c *Claims) UnmarshalJSON(data []byte) error { aux := struct { UsersID string `json:"users_id"` UserID string `json:"user_id"` EmailAddress string `json:"email_address"` Email string `json:"email"` RoleID RoleIDs `json:"role_id"` AdditionalRoleID RoleIDs `json:"additional_role_id"` Projects json.RawMessage `json:"projects"` ProjectMetadata json.RawMessage `json:"project_metadata"` ProjectsMetadata json.RawMessage `json:"projects_metadata"` jwt.RegisteredClaims }{} if err := json.Unmarshal(data, &aux); err != nil { return err } c.UsersID = aux.UsersID c.EmailAddress = aux.EmailAddress c.RoleID = aux.RoleID c.AdditionalRoleID = aux.AdditionalRoleID c.RegisteredClaims = aux.RegisteredClaims if c.UsersID == "" && aux.UserID != "" { c.UsersID = aux.UserID } if c.EmailAddress == "" && aux.Email != "" { c.EmailAddress = aux.Email } projects := make([]ProjectClaim, 0) rawProjectFields := []json.RawMessage{aux.Projects, aux.ProjectMetadata, aux.ProjectsMetadata} for _, raw := range rawProjectFields { parsedProjects, err := parseProjectClaims(raw) if err != nil { return err } if len(parsedProjects) > 0 { projects = append(projects, parsedProjects...) } } c.Projects = projects return nil } func parseProjectClaims(raw json.RawMessage) ([]ProjectClaim, error) { trimmed := bytes.TrimSpace(raw) if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) { return nil, nil } switch trimmed[0] { case '[': var projects []ProjectClaim if err := json.Unmarshal(trimmed, &projects); err != nil { return nil, err } return projects, nil case '{': var project ProjectClaim if err := json.Unmarshal(trimmed, &project); err != nil { return nil, err } return []ProjectClaim{project}, nil default: return nil, fmt.Errorf("unsupported JSON for projects claim: %s", string(trimmed)) } } // ContextKey is a custom type for context keys to avoid collisions type ContextKey string // CacheEntry represents a token cache entry type CacheEntry struct { Claims *Claims ExpiresAt time.Time }