Skip to content

Commit f203f2d

Browse files
Merge branch 'main' into feature/portal
- Get changes sort feature to portal branch
2 parents df3877e + 3af4341 commit f203f2d

18 files changed

Lines changed: 1128 additions & 4 deletions

File tree

api/consent-management-API.yaml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,13 @@ paths:
206206
type: integer
207207
format: int64
208208
example: 1734422400000
209+
- name: sort
210+
in: query
211+
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`.
212+
schema:
213+
type: string
214+
default: createdTime:desc
215+
example: "status:asc,createdTime:desc"
209216
- name: limit
210217
in: query
211218
description: The maximum number of results to return in a single page. Used for pagination.
@@ -361,6 +368,53 @@ paths:
361368
$ref: "#/components/schemas/ConsentErrorCommon"
362369
security:
363370
- basicAuth: []
371+
/consents/group-ids:
372+
get:
373+
summary: Retrieve group IDs for a user
374+
description: |
375+
Retrieves the distinct group IDs associated with the specified user ID.
376+
377+
Results are scoped to the requesting organization and include only group IDs.
378+
Group metadata is not returned.
379+
operationId: consents-group-ids-GET
380+
tags:
381+
- Consents
382+
parameters:
383+
- in: header
384+
name: org-id
385+
required: true
386+
description: "The unique identifier for the organization."
387+
schema:
388+
type: string
389+
example: "ORG-001"
390+
- name: userId
391+
in: query
392+
required: true
393+
description: The user ID to search for.
394+
schema:
395+
type: string
396+
example: "user1@example.com"
397+
responses:
398+
"200":
399+
description: Successfully retrieved the group IDs matching the search criteria.
400+
content:
401+
application/json:
402+
schema:
403+
$ref: "#/components/schemas/ConsentGroupIDsResponse"
404+
"400":
405+
description: Bad Request. The userId parameter is missing, repeated, or invalid.
406+
content:
407+
application/json:
408+
schema:
409+
$ref: "#/components/schemas/ConsentErrorCommon"
410+
"500":
411+
description: Internal Server Error.
412+
content:
413+
application/json:
414+
schema:
415+
$ref: "#/components/schemas/ConsentErrorCommon"
416+
security:
417+
- basicAuth: []
364418
/consents/{consentId}:
365419
get:
366420
summary: Retrieve a consent by its ID
@@ -4138,6 +4192,24 @@ components:
41384192
example: 3
41394193
ConsentAttributeSearchResponse:
41404194
$ref: "#/components/schemas/ConsentIdsResponse"
4195+
ConsentGroupIDsResponse:
4196+
type: object
4197+
description: Response payload containing group IDs matching the search criteria.
4198+
required:
4199+
- groupIds
4200+
- count
4201+
properties:
4202+
groupIds:
4203+
description: Array of distinct group IDs, sorted alphabetically.
4204+
type: array
4205+
uniqueItems: true
4206+
items:
4207+
type: string
4208+
example: ["group-001", "group-002"]
4209+
count:
4210+
description: The number of distinct group IDs returned.
4211+
type: integer
4212+
example: 2
41414213
ConsentElementCreateRequest:
41424214
type: object
41434215
required:

consent-server/internal/consent/ConsentSevice_mock_test.go

Lines changed: 62 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

consent-server/internal/consent/handler.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,23 @@ func (h *consentHandler) listConsents(w http.ResponseWriter, r *http.Request) {
251251
}
252252
}
253253

254+
sortParams := r.URL.Query()["sort"]
255+
if len(sortParams) > 1 {
256+
utils.SendError(w, r, serviceerror.CustomServiceError(ErrorValidationFailed, "exactly one sort parameter is allowed"))
257+
return
258+
}
259+
sortParam := ""
260+
if len(sortParams) == 1 {
261+
sortParam = sortParams[0]
262+
}
263+
264+
sorts, err := parseConsentSorts(sortParam)
265+
if err != nil {
266+
utils.SendError(w, r, serviceerror.CustomServiceError(ErrorValidationFailed, err.Error()))
267+
return
268+
}
269+
filters.Sort = sorts
270+
254271
// purposeVersion requires purposeName
255272
if filters.PurposeVersion != nil && filters.PurposeName == "" {
256273
utils.SendError(w, r, serviceerror.CustomServiceError(ErrorValidationFailed,
@@ -417,6 +434,40 @@ func (h *consentHandler) searchConsentsByAttribute(w http.ResponseWriter, r *htt
417434
})
418435
}
419436

437+
// getGroupIDsByUserID handles GET /consents/group-ids
438+
func (h *consentHandler) getGroupIDsByUserID(w http.ResponseWriter, r *http.Request) {
439+
ctx := r.Context()
440+
orgID := r.Header.Get(constants.HeaderOrgID)
441+
442+
if err := utils.ValidateOrgID(orgID); err != nil {
443+
utils.SendError(w, r, serviceerror.CustomServiceError(ErrorValidationFailed, err.Error()))
444+
return
445+
}
446+
447+
userIDs := r.URL.Query()["userId"]
448+
if len(userIDs) == 0 || userIDs[0] == "" {
449+
utils.SendError(w, r, serviceerror.CustomServiceError(ErrorValidationFailed, "userId parameter is required"))
450+
return
451+
}
452+
if len(userIDs) > 1 {
453+
utils.SendError(w, r, serviceerror.CustomServiceError(ErrorValidationFailed, "exactly one userId parameter is required"))
454+
return
455+
}
456+
457+
out, serviceErr := h.service.GetGroupIDsByUserID(ctx, userIDs[0], orgID)
458+
if serviceErr != nil {
459+
utils.SendError(w, r, serviceErr)
460+
return
461+
}
462+
463+
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
464+
w.WriteHeader(http.StatusOK)
465+
json.NewEncoder(w).Encode(&model.ConsentGroupIDsResponse{
466+
GroupIDs: out.GroupIDs,
467+
Count: out.Count,
468+
})
469+
}
470+
420471
// =============================================================================
421472
// Request → service input converters
422473
// =============================================================================
@@ -480,6 +531,83 @@ func requestToUpdateInput(req model.ConsentUpdateRequest) (model.UpdateConsentIn
480531
}, nil
481532
}
482533

534+
func parseConsentSorts(raw string) ([]model.ConsentSort, error) {
535+
const maxConsentSortFields = 3
536+
537+
if strings.TrimSpace(raw) == "" {
538+
return []model.ConsentSort{{
539+
Field: model.ConsentSortFieldCreatedTime,
540+
Direction: model.ConsentSortDirectionDesc,
541+
}}, nil
542+
}
543+
544+
supportedFields := map[string]model.ConsentSortField{
545+
string(model.ConsentSortFieldCreatedTime): model.ConsentSortFieldCreatedTime,
546+
string(model.ConsentSortFieldUpdatedTime): model.ConsentSortFieldUpdatedTime,
547+
string(model.ConsentSortFieldValidityTime): model.ConsentSortFieldValidityTime,
548+
string(model.ConsentSortFieldStatus): model.ConsentSortFieldStatus,
549+
string(model.ConsentSortFieldGroupID): model.ConsentSortFieldGroupID,
550+
string(model.ConsentSortFieldConsentType): model.ConsentSortFieldConsentType,
551+
}
552+
553+
items := strings.Split(raw, ",")
554+
if len(items) > maxConsentSortFields {
555+
return nil, fmt.Errorf("a maximum of %d sort fields is allowed", maxConsentSortFields)
556+
}
557+
558+
sorts := make([]model.ConsentSort, 0, len(items))
559+
seenFields := make(map[model.ConsentSortField]struct{}, len(items))
560+
561+
for _, item := range items {
562+
item = strings.TrimSpace(item)
563+
if item == "" {
564+
return nil, fmt.Errorf("sort contains an empty item")
565+
}
566+
567+
parts := strings.Split(item, ":")
568+
if len(parts) > 2 {
569+
return nil, fmt.Errorf("invalid sort item %q", item)
570+
}
571+
572+
fieldName := strings.TrimSpace(parts[0])
573+
if fieldName == "" {
574+
return nil, fmt.Errorf("sort field is required")
575+
}
576+
577+
field, ok := supportedFields[fieldName]
578+
if !ok {
579+
return nil, fmt.Errorf("unsupported sort field %q", fieldName)
580+
}
581+
if _, exists := seenFields[field]; exists {
582+
return nil, fmt.Errorf("duplicate sort field %q", fieldName)
583+
}
584+
585+
direction := model.ConsentSortDirectionDesc
586+
if len(parts) == 2 {
587+
rawDirection := strings.TrimSpace(parts[1])
588+
if rawDirection == "" {
589+
return nil, fmt.Errorf("sort direction is required when ':' is used")
590+
}
591+
switch rawDirection {
592+
case "asc":
593+
direction = model.ConsentSortDirectionAsc
594+
case "desc":
595+
direction = model.ConsentSortDirectionDesc
596+
default:
597+
return nil, fmt.Errorf("unsupported sort direction %q", rawDirection)
598+
}
599+
}
600+
601+
sorts = append(sorts, model.ConsentSort{
602+
Field: field,
603+
Direction: direction,
604+
})
605+
seenFields[field] = struct{}{}
606+
}
607+
608+
return sorts, nil
609+
}
610+
483611
// parsePurposeRefRequests converts API purpose references to service-layer input structs.
484612
// Version strings ("v1", "v2", …) are parsed into integer version numbers.
485613
func parsePurposeRefRequests(reqs []model.ConsentPurposeRefRequest) ([]model.ConsentPurposeInput, error) {

0 commit comments

Comments
 (0)