Add consent records tab to cookie banner config
Exposes the cookie consent record audit trail through a new "Consent Records" tab on the cookie banner configuration page. The full stack includes: extended coredata filter (visitor ID, banner version), GraphQL schema/types/resolvers, and a React page with SortableTable (size 50) and three compliance filters (action, visitor ID, banner version). Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/cookiebanner"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
||||
@@ -122,6 +123,50 @@ func (r *cookieBannerResolver) LatestVersion(ctx context.Context, obj *types.Coo
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConsentRecords is the resolver for the consentRecords field.
|
||||
func (r *cookieBannerResolver) ConsentRecords(ctx context.Context, obj *types.CookieBanner, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.CookieConsentRecordOrderBy, filter *types.CookieConsentRecordFilter) (*types.CookieConsentRecordConnection, error) {
|
||||
if err := r.authorize(ctx, obj.ID, probo.ActionCookieConsentRecordList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pageOrderBy := page.OrderBy[coredata.CookieConsentRecordOrderField]{
|
||||
Field: coredata.CookieConsentRecordOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
if orderBy != nil {
|
||||
pageOrderBy = page.OrderBy[coredata.CookieConsentRecordOrderField]{
|
||||
Field: orderBy.Field,
|
||||
Direction: orderBy.Direction,
|
||||
}
|
||||
}
|
||||
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
scope := coredata.NewScopeFromObjectID(obj.ID)
|
||||
|
||||
var (
|
||||
action *coredata.CookieConsentAction
|
||||
visitorID *string
|
||||
cookieBannerVersionID *gid.GID
|
||||
)
|
||||
if filter != nil {
|
||||
action = filter.Action
|
||||
visitorID = filter.VisitorID
|
||||
cookieBannerVersionID = filter.CookieBannerVersionID
|
||||
}
|
||||
|
||||
coredataFilter := coredata.NewCookieConsentRecordFilter(action, visitorID, cookieBannerVersionID)
|
||||
|
||||
records, err := r.cookieBanner.ListCookieConsentRecordsForBanner(ctx, scope, obj.ID, cursor, coredataFilter)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list consent records", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
p := page.NewPage(records, cursor)
|
||||
|
||||
return types.NewCookieConsentRecordConnection(p, r, obj.ID, coredataFilter), nil
|
||||
}
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
func (r *cookieBannerResolver) Permission(ctx context.Context, obj *types.CookieBanner, action string) (bool, error) {
|
||||
return r.Resolver.Permission(ctx, obj, action)
|
||||
|
||||
57
pkg/server/api/console/v1/cookie_consent_record_resolvers.go
Normal file
57
pkg/server/api/console/v1/cookie_consent_record_resolvers.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package console_v1
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver
|
||||
// implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
// Code generated by github.com/99designs/gqlgen version v0.17.87
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
)
|
||||
|
||||
// CookieBanner is the resolver for the cookieBanner field.
|
||||
func (r *cookieConsentRecordResolver) CookieBanner(ctx context.Context, obj *types.CookieConsentRecord) (*types.CookieBanner, error) {
|
||||
return obj.CookieBanner, nil
|
||||
}
|
||||
|
||||
// CookieBannerVersion is the resolver for the cookieBannerVersion field.
|
||||
func (r *cookieConsentRecordResolver) CookieBannerVersion(ctx context.Context, obj *types.CookieConsentRecord) (*types.CookieBannerVersion, error) {
|
||||
return obj.CookieBannerVersion, nil
|
||||
}
|
||||
|
||||
// TotalCount is the resolver for the totalCount field.
|
||||
func (r *cookieConsentRecordConnectionResolver) TotalCount(ctx context.Context, obj *types.CookieConsentRecordConnection) (int, error) {
|
||||
if err := r.authorize(ctx, obj.ParentID, probo.ActionCookieConsentRecordList); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
scope := coredata.NewScopeFromObjectID(obj.ParentID)
|
||||
|
||||
count, err := r.cookieBanner.CountCookieConsentRecordsForBanner(ctx, scope, obj.ParentID, obj.Filter)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot count consent records", log.Error(err))
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CookieConsentRecord returns schema.CookieConsentRecordResolver implementation.
|
||||
func (r *Resolver) CookieConsentRecord() schema.CookieConsentRecordResolver {
|
||||
return &cookieConsentRecordResolver{r}
|
||||
}
|
||||
|
||||
// CookieConsentRecordConnection returns schema.CookieConsentRecordConnectionResolver implementation.
|
||||
func (r *Resolver) CookieConsentRecordConnection() schema.CookieConsentRecordConnectionResolver {
|
||||
return &cookieConsentRecordConnectionResolver{r}
|
||||
}
|
||||
|
||||
type cookieConsentRecordResolver struct{ *Resolver }
|
||||
type cookieConsentRecordConnectionResolver struct{ *Resolver }
|
||||
@@ -102,6 +102,15 @@ type CookieBanner implements Node {
|
||||
|
||||
latestVersion: CookieBannerVersion @goField(forceResolver: true)
|
||||
|
||||
consentRecords(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: CookieConsentRecordOrder
|
||||
filter: CookieConsentRecordFilter
|
||||
): CookieConsentRecordConnection @goField(forceResolver: true)
|
||||
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
enum CookieConsentAction
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.CookieConsentAction") {
|
||||
ACCEPT_ALL
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.CookieConsentActionAcceptAll"
|
||||
)
|
||||
REJECT_ALL
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.CookieConsentActionRejectAll"
|
||||
)
|
||||
CUSTOMIZE
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.CookieConsentActionCustomize"
|
||||
)
|
||||
GPC
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.CookieConsentActionGPC"
|
||||
)
|
||||
}
|
||||
|
||||
enum CookieConsentRecordOrderField
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.CookieConsentRecordOrderField"
|
||||
) {
|
||||
CREATED_AT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.CookieConsentRecordOrderFieldCreatedAt"
|
||||
)
|
||||
}
|
||||
|
||||
input CookieConsentRecordOrder
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CookieConsentRecordOrderBy"
|
||||
) {
|
||||
direction: OrderDirection!
|
||||
field: CookieConsentRecordOrderField!
|
||||
}
|
||||
|
||||
input CookieConsentRecordFilter {
|
||||
action: CookieConsentAction
|
||||
visitorId: String
|
||||
cookieBannerVersionId: ID
|
||||
}
|
||||
|
||||
type CookieConsentRecord implements Node {
|
||||
id: ID!
|
||||
cookieBanner: CookieBanner @goField(forceResolver: true)
|
||||
cookieBannerVersion: CookieBannerVersion @goField(forceResolver: true)
|
||||
visitorId: String!
|
||||
ipAddress: String
|
||||
userAgent: String
|
||||
consentData: String!
|
||||
action: CookieConsentAction!
|
||||
sdkVersion: String!
|
||||
createdAt: Datetime!
|
||||
}
|
||||
|
||||
type CookieConsentRecordConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CookieConsentRecordConnection"
|
||||
) {
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
edges: [CookieConsentRecordEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type CookieConsentRecordEdge {
|
||||
cursor: CursorKey!
|
||||
node: CookieConsentRecord!
|
||||
}
|
||||
86
pkg/server/api/console/v1/types/cookie_consent_record.go
Normal file
86
pkg/server/api/console/v1/types/cookie_consent_record.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
CookieConsentRecordOrderBy OrderBy[coredata.CookieConsentRecordOrderField]
|
||||
|
||||
CookieConsentRecordConnection struct {
|
||||
TotalCount int
|
||||
Edges []*CookieConsentRecordEdge
|
||||
PageInfo PageInfo
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
Filter *coredata.CookieConsentRecordFilter
|
||||
}
|
||||
)
|
||||
|
||||
func NewCookieConsentRecordConnection(
|
||||
p *page.Page[*coredata.CookieConsentRecord, coredata.CookieConsentRecordOrderField],
|
||||
parentType any,
|
||||
parentID gid.GID,
|
||||
filter *coredata.CookieConsentRecordFilter,
|
||||
) *CookieConsentRecordConnection {
|
||||
edges := make([]*CookieConsentRecordEdge, len(p.Data))
|
||||
|
||||
for i := range edges {
|
||||
edges[i] = NewCookieConsentRecordEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return &CookieConsentRecordConnection{
|
||||
Edges: edges,
|
||||
PageInfo: *NewPageInfo(p),
|
||||
|
||||
Resolver: parentType,
|
||||
ParentID: parentID,
|
||||
Filter: filter,
|
||||
}
|
||||
}
|
||||
|
||||
func NewCookieConsentRecordEdge(
|
||||
r *coredata.CookieConsentRecord,
|
||||
orderBy coredata.CookieConsentRecordOrderField,
|
||||
) *CookieConsentRecordEdge {
|
||||
return &CookieConsentRecordEdge{
|
||||
Cursor: r.CursorKey(orderBy),
|
||||
Node: NewCookieConsentRecord(r),
|
||||
}
|
||||
}
|
||||
|
||||
func NewCookieConsentRecord(r *coredata.CookieConsentRecord) *CookieConsentRecord {
|
||||
return &CookieConsentRecord{
|
||||
ID: r.ID,
|
||||
CookieBanner: &CookieBanner{
|
||||
ID: r.CookieBannerID,
|
||||
},
|
||||
CookieBannerVersion: &CookieBannerVersion{
|
||||
ID: r.CookieBannerVersionID,
|
||||
},
|
||||
VisitorID: r.VisitorID,
|
||||
IPAddress: r.IPAddress,
|
||||
UserAgent: r.UserAgent,
|
||||
ConsentData: string(r.ConsentData),
|
||||
Action: r.Action,
|
||||
SdkVersion: r.SdkVersion,
|
||||
CreatedAt: r.CreatedAt,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user