Add consent record detail page
Display record attributes and parsed consent data with per-category consent state and cookies from the banner version snapshot. The page lives outside the config layout with its own breadcrumb navigation. Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -1738,6 +1738,34 @@ func (s *Service) ListCookieConsentRecordsForBanner(
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetCookieConsentRecord(
|
||||
ctx context.Context,
|
||||
scope coredata.Scoper,
|
||||
id gid.GID,
|
||||
) (*coredata.CookieConsentRecord, error) {
|
||||
var record coredata.CookieConsentRecord
|
||||
|
||||
err := s.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
if err := record.LoadByID(ctx, conn, scope, id); err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return ErrConsentNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load consent record: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
func (s *Service) CountCookieConsentRecordsForBanner(
|
||||
ctx context.Context,
|
||||
scope coredata.Scoper,
|
||||
|
||||
@@ -56,7 +56,18 @@ func (r *CookieConsentRecord) CursorKey(field CookieConsentRecordOrderField) pag
|
||||
}
|
||||
|
||||
func (r *CookieConsentRecord) AuthorizationAttributes(ctx context.Context, conn pg.Querier) (map[string]string, error) {
|
||||
return map[string]string{"organization_id": r.OrganizationID.String()}, nil
|
||||
q := `SELECT organization_id FROM cookie_consent_records WHERE id = $1 LIMIT 1;`
|
||||
|
||||
var organizationID gid.GID
|
||||
if err := conn.QueryRow(ctx, q, r.ID).Scan(&organizationID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, ErrResourceNotFound
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("cannot query consent record authorization attributes: %w", err)
|
||||
}
|
||||
|
||||
return map[string]string{"organization_id": organizationID.String()}, nil
|
||||
}
|
||||
|
||||
func (r *CookieConsentRecords) LoadByCookieBannerID(
|
||||
@@ -203,6 +214,56 @@ INSERT INTO cookie_consent_records (
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *CookieConsentRecord) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
id gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
cookie_banner_id,
|
||||
cookie_banner_version_id,
|
||||
visitor_id,
|
||||
ip_address,
|
||||
user_agent,
|
||||
consent_data,
|
||||
action,
|
||||
sdk_version,
|
||||
created_at
|
||||
FROM
|
||||
cookie_consent_records
|
||||
WHERE
|
||||
%s
|
||||
AND id = @id
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"id": id}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query consent record: %w", err)
|
||||
}
|
||||
|
||||
record, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CookieConsentRecord])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect consent record: %w", err)
|
||||
}
|
||||
|
||||
*r = record
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *CookieConsentRecord) LoadLatestByVisitorAndBannerID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
|
||||
@@ -352,6 +352,16 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
|
||||
}
|
||||
return types.NewCookieCategory(category), nil
|
||||
}
|
||||
case coredata.CookieConsentRecordEntityType:
|
||||
action = probo.ActionCookieConsentRecordList
|
||||
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
|
||||
scope := coredata.NewScopeFromObjectID(id)
|
||||
record, err := r.cookieBanner.GetCookieConsentRecord(ctx, scope, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return types.NewCookieConsentRecord(record), nil
|
||||
}
|
||||
case coredata.CookieBannerVersionEntityType:
|
||||
action = probo.ActionCookieBannerVersionGet
|
||||
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
|
||||
|
||||
@@ -188,6 +188,45 @@ func (r *cookieBannerConnectionResolver) TotalCount(ctx context.Context, obj *ty
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Categories is the resolver for the categories field.
|
||||
func (r *cookieBannerVersionResolver) Categories(ctx context.Context, obj *types.CookieBannerVersion) ([]*types.CookieBannerVersionCategory, error) {
|
||||
scope := coredata.NewScopeFromObjectID(obj.ID)
|
||||
|
||||
version, err := r.cookieBanner.GetCookieBannerVersion(ctx, scope, obj.ID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot get cookie banner version", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
snapshot, err := version.GetSnapshot()
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot get version snapshot", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
categories := make([]*types.CookieBannerVersionCategory, len(snapshot.Categories))
|
||||
for i, cat := range snapshot.Categories {
|
||||
cookies := make([]*types.CookieBannerVersionCookie, len(cat.Cookies))
|
||||
for j, c := range cat.Cookies {
|
||||
cookies[j] = &types.CookieBannerVersionCookie{
|
||||
Name: c.Name,
|
||||
Duration: c.Duration,
|
||||
Description: c.Description,
|
||||
}
|
||||
}
|
||||
|
||||
categories[i] = &types.CookieBannerVersionCategory{
|
||||
Name: cat.Name,
|
||||
Slug: cat.Slug,
|
||||
Description: cat.Description,
|
||||
Kind: cat.Kind,
|
||||
Cookies: cookies,
|
||||
}
|
||||
}
|
||||
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
// CookieBanner is the resolver for the cookieBanner field.
|
||||
func (r *cookieCategoryResolver) CookieBanner(ctx context.Context, obj *types.CookieCategory) (*types.CookieBanner, error) {
|
||||
return obj.CookieBanner, nil
|
||||
@@ -848,6 +887,11 @@ func (r *Resolver) CookieBannerConnection() schema.CookieBannerConnectionResolve
|
||||
return &cookieBannerConnectionResolver{r}
|
||||
}
|
||||
|
||||
// CookieBannerVersion returns schema.CookieBannerVersionResolver implementation.
|
||||
func (r *Resolver) CookieBannerVersion() schema.CookieBannerVersionResolver {
|
||||
return &cookieBannerVersionResolver{r}
|
||||
}
|
||||
|
||||
// CookieCategory returns schema.CookieCategoryResolver implementation.
|
||||
func (r *Resolver) CookieCategory() schema.CookieCategoryResolver { return &cookieCategoryResolver{r} }
|
||||
|
||||
@@ -864,6 +908,7 @@ func (r *Resolver) CookieConnection() schema.CookieConnectionResolver {
|
||||
type cookieResolver struct{ *Resolver }
|
||||
type cookieBannerResolver struct{ *Resolver }
|
||||
type cookieBannerConnectionResolver struct{ *Resolver }
|
||||
type cookieBannerVersionResolver struct{ *Resolver }
|
||||
type cookieCategoryResolver struct{ *Resolver }
|
||||
type cookieCategoryConnectionResolver struct{ *Resolver }
|
||||
type cookieConnectionResolver struct{ *Resolver }
|
||||
|
||||
@@ -20,7 +20,26 @@ import (
|
||||
|
||||
// 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
|
||||
if obj.CookieBanner == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := r.authorize(ctx, obj.CookieBanner.ID, probo.ActionCookieBannerGet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scope := coredata.NewScopeFromObjectID(obj.CookieBanner.ID)
|
||||
|
||||
banner, err := r.cookieBanner.GetCookieBanner(ctx, scope, obj.CookieBanner.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, cookiebanner.ErrBannerNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot get cookie banner", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewCookieBanner(banner), nil
|
||||
}
|
||||
|
||||
// CookieBannerVersion is the resolver for the cookieBannerVersion field.
|
||||
|
||||
@@ -198,10 +198,25 @@ type CookieBannerVersion implements Node {
|
||||
id: ID!
|
||||
version: Int!
|
||||
state: String!
|
||||
categories: [CookieBannerVersionCategory!]! @goField(forceResolver: true)
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
|
||||
type CookieBannerVersionCategory {
|
||||
name: String!
|
||||
slug: String!
|
||||
description: String!
|
||||
kind: CookieCategoryKind!
|
||||
cookies: [CookieBannerVersionCookie!]!
|
||||
}
|
||||
|
||||
type CookieBannerVersionCookie {
|
||||
name: String!
|
||||
duration: String!
|
||||
description: String!
|
||||
}
|
||||
|
||||
type CookieBannerConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CookieBannerConnection"
|
||||
|
||||
Reference in New Issue
Block a user