Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions api/consent-management-API.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,13 @@ paths:
type: integer
format: int64
example: 1734422400000
- name: sort
in: query
description: Sort results using a comma-separated list of sort items. Each item can be either `{field}:{direction}` or `{field}`, which defaults to `desc`. Supported fields are `createdTime`, `updatedTime`, `validityTime`, `status`, `groupId`, and `consentType`. Supported directions are `asc` and `desc`. A maximum of 3 sort items is allowed. If `validityTime` is used, consents without an expiry are treated as having the latest validity time. If omitted, results are sorted by `createdTime:desc`.
schema:
type: string
default: createdTime:desc
example: "status:asc,createdTime:desc"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: limit
in: query
description: The maximum number of results to return in a single page. Used for pagination.
Expand Down Expand Up @@ -295,6 +302,53 @@ paths:
$ref: "#/components/schemas/ConsentErrorCommon"
security:
- basicAuth: []
/consents/group-ids:
get:
summary: Retrieve group IDs for a user
description: |
Retrieves the distinct group IDs associated with the specified user ID.

Results are scoped to the requesting organization and include only group IDs.
Group metadata is not returned.
operationId: consents-group-ids-GET
tags:
- Consents
parameters:
- in: header
name: org-id
required: true
description: "The unique identifier for the organization."
schema:
type: string
example: "ORG-001"
- name: userId
in: query
required: true
description: The user ID to search for.
schema:
type: string
example: "user1@example.com"
responses:
"200":
description: Successfully retrieved the group IDs matching the search criteria.
content:
application/json:
schema:
$ref: "#/components/schemas/ConsentGroupIDsResponse"
"400":
description: Bad Request. The userId parameter is missing, repeated, or invalid.
content:
application/json:
schema:
$ref: "#/components/schemas/ConsentErrorCommon"
"500":
description: Internal Server Error.
content:
application/json:
schema:
$ref: "#/components/schemas/ConsentErrorCommon"
security:
- basicAuth: []
/consents/{consentId}:
get:
summary: Retrieve a consent by its ID
Expand Down Expand Up @@ -3938,6 +3992,24 @@ components:
example: 3
ConsentAttributeSearchResponse:
$ref: "#/components/schemas/ConsentIdsResponse"
ConsentGroupIDsResponse:
type: object
description: Response payload containing group IDs matching the search criteria.
required:
- groupIds
- count
properties:
groupIds:
description: Array of distinct group IDs, sorted alphabetically.
type: array
uniqueItems: true
items:
type: string
example: ["group-001", "group-002"]
count:
description: The number of distinct group IDs returned.
type: integer
example: 2
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ConsentElementCreateRequest:
type: object
required:
Expand Down
62 changes: 62 additions & 0 deletions consent-server/internal/consent/ConsentSevice_mock_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

128 changes: 128 additions & 0 deletions consent-server/internal/consent/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,23 @@ func (h *consentHandler) listConsents(w http.ResponseWriter, r *http.Request) {
}
}

sortParams := r.URL.Query()["sort"]
if len(sortParams) > 1 {
utils.SendError(w, r, serviceerror.CustomServiceError(ErrorValidationFailed, "exactly one sort parameter is allowed"))
return
}
sortParam := ""
if len(sortParams) == 1 {
sortParam = sortParams[0]
}

sorts, err := parseConsentSorts(sortParam)
if err != nil {
utils.SendError(w, r, serviceerror.CustomServiceError(ErrorValidationFailed, err.Error()))
return
}
filters.Sort = sorts

// purposeVersion requires purposeName
if filters.PurposeVersion != nil && filters.PurposeName == "" {
utils.SendError(w, r, serviceerror.CustomServiceError(ErrorValidationFailed,
Expand Down Expand Up @@ -415,6 +432,40 @@ func (h *consentHandler) searchConsentsByAttribute(w http.ResponseWriter, r *htt
})
}

// getGroupIDsByUserID handles GET /consents/group-ids
func (h *consentHandler) getGroupIDsByUserID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
orgID := r.Header.Get(constants.HeaderOrgID)

if err := utils.ValidateOrgID(orgID); err != nil {
utils.SendError(w, r, serviceerror.CustomServiceError(ErrorValidationFailed, err.Error()))
return
}

userIDs := r.URL.Query()["userId"]
if len(userIDs) == 0 || userIDs[0] == "" {
utils.SendError(w, r, serviceerror.CustomServiceError(ErrorValidationFailed, "userId parameter is required"))
return
}
if len(userIDs) > 1 {
utils.SendError(w, r, serviceerror.CustomServiceError(ErrorValidationFailed, "exactly one userId parameter is required"))
return
}

out, serviceErr := h.service.GetGroupIDsByUserID(ctx, userIDs[0], orgID)
if serviceErr != nil {
utils.SendError(w, r, serviceErr)
return
}

w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(&model.ConsentGroupIDsResponse{
GroupIDs: out.GroupIDs,
Count: out.Count,
})
}

// =============================================================================
// Request → service input converters
// =============================================================================
Expand Down Expand Up @@ -478,6 +529,83 @@ func requestToUpdateInput(req model.ConsentUpdateRequest) (model.UpdateConsentIn
}, nil
}

func parseConsentSorts(raw string) ([]model.ConsentSort, error) {
const maxConsentSortFields = 3

if strings.TrimSpace(raw) == "" {
return []model.ConsentSort{{
Field: model.ConsentSortFieldCreatedTime,
Direction: model.ConsentSortDirectionDesc,
}}, nil
}

supportedFields := map[string]model.ConsentSortField{
string(model.ConsentSortFieldCreatedTime): model.ConsentSortFieldCreatedTime,
string(model.ConsentSortFieldUpdatedTime): model.ConsentSortFieldUpdatedTime,
string(model.ConsentSortFieldValidityTime): model.ConsentSortFieldValidityTime,
string(model.ConsentSortFieldStatus): model.ConsentSortFieldStatus,
string(model.ConsentSortFieldGroupID): model.ConsentSortFieldGroupID,
string(model.ConsentSortFieldConsentType): model.ConsentSortFieldConsentType,
}

items := strings.Split(raw, ",")
if len(items) > maxConsentSortFields {
return nil, fmt.Errorf("a maximum of %d sort fields is allowed", maxConsentSortFields)
}

sorts := make([]model.ConsentSort, 0, len(items))
seenFields := make(map[model.ConsentSortField]struct{}, len(items))

for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
return nil, fmt.Errorf("sort contains an empty item")
}

parts := strings.Split(item, ":")
if len(parts) > 2 {
return nil, fmt.Errorf("invalid sort item %q", item)
}

fieldName := strings.TrimSpace(parts[0])
if fieldName == "" {
return nil, fmt.Errorf("sort field is required")
}

field, ok := supportedFields[fieldName]
if !ok {
return nil, fmt.Errorf("unsupported sort field %q", fieldName)
}
if _, exists := seenFields[field]; exists {
return nil, fmt.Errorf("duplicate sort field %q", fieldName)
}

direction := model.ConsentSortDirectionDesc
if len(parts) == 2 {
rawDirection := strings.TrimSpace(parts[1])
if rawDirection == "" {
return nil, fmt.Errorf("sort direction is required when ':' is used")
}
switch rawDirection {
case "asc":
direction = model.ConsentSortDirectionAsc
case "desc":
direction = model.ConsentSortDirectionDesc
default:
return nil, fmt.Errorf("unsupported sort direction %q", rawDirection)
}
}

sorts = append(sorts, model.ConsentSort{
Field: field,
Direction: direction,
})
seenFields[field] = struct{}{}
}

return sorts, nil
}

// parsePurposeRefRequests converts API purpose references to service-layer input structs.
// Version strings ("v1", "v2", …) are parsed into integer version numbers.
func parsePurposeRefRequests(reqs []model.ConsentPurposeRefRequest) ([]model.ConsentPurposeInput, error) {
Expand Down
Loading