Add compliance page mailing list base

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-02-25 14:59:00 +01:00
parent c0a69100e8
commit 4d2cb793b6
50 changed files with 6365 additions and 110 deletions

View File

@@ -20,19 +20,21 @@ import (
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/server/api/authz"
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
"go.probo.inc/probo/pkg/server/gqlutils"
)
func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, esignSvc *esign.Service, customDomainCname string, logger *log.Logger) http.Handler {
func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, esignSvc *esign.Service, mailmanSvc *mailman.Service, customDomainCname string, logger *log.Logger) http.Handler {
config := schema.Config{
Resolvers: &Resolver{
authorize: authz.NewAuthorizeFunc(iamSvc, logger),
probo: proboSvc,
iam: iamSvc,
esign: esignSvc,
mailman: mailmanSvc,
customDomainCname: customDomainCname,
logger: logger,
},

View File

@@ -31,6 +31,7 @@ import (
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/saferedirect"
"go.probo.inc/probo/pkg/securecookie"
@@ -45,6 +46,7 @@ type (
probo *probo.Service
iam *iam.Service
esign *esign.Service
mailman *mailman.Service
logger *log.Logger
customDomainCname string
}
@@ -55,6 +57,7 @@ func NewMux(
proboSvc *probo.Service,
iamSvc *iam.Service,
esignSvc *esign.Service,
mailmanSvc *mailman.Service,
cookieConfig securecookie.Config,
tokenSecret string,
connectorRegistry *connector.ConnectorRegistry,
@@ -65,7 +68,7 @@ func NewMux(
safeRedirect := &saferedirect.SafeRedirect{AllowedHost: baseURL.Host()}
graphqlHandler := NewGraphQLHandler(iamSvc, proboSvc, esignSvc, customDomainCname, logger)
graphqlHandler := NewGraphQLHandler(iamSvc, proboSvc, esignSvc, mailmanSvc, customDomainCname, logger)
r.Group(func(r chi.Router) {
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
@@ -199,6 +202,10 @@ func (r *Resolver) ProboService(ctx context.Context, tenantID gid.TenantID) *pro
return r.probo.WithTenant(tenantID)
}
func (r *Resolver) MailmanService(ctx context.Context, tenantID gid.TenantID) *mailman.TenantService {
return r.mailman.WithTenant(tenantID)
}
func (r *Resolver) Permission(ctx context.Context, obj types.Node, action string) (bool, error) {
return r.authorize(ctx, obj.GetID(), action) == nil, nil
}

View File

@@ -1634,7 +1634,10 @@ input VendorFilter {
}
# Core Types
type TrustCenter implements Node {
type TrustCenter implements Node
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.TrustCenter"
) {
id: ID!
active: Boolean!
logoFileUrl: String @goField(forceResolver: true)
@@ -1677,9 +1680,60 @@ type TrustCenter implements Node {
orderBy: ComplianceExternalURLOrder
): ComplianceExternalURLConnection! @goField(forceResolver: true)
mailingList: MailingList @goField(forceResolver: true)
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type MailingList implements Node {
id: ID!
replyTo: EmailAddr
subscribers(
first: Int
after: CursorKey
last: Int
before: CursorKey
): MailingListSubscriberConnection! @goField(forceResolver: true)
}
enum MailingListSubscriberStatus
@goModel(
model: "go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatus"
) {
PENDING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatusPending"
)
CONFIRMED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatusConfirmed"
)
}
type MailingListSubscriber implements Node {
id: ID!
fullName: String!
email: EmailAddr!
status: MailingListSubscriberStatus!
createdAt: Datetime!
updatedAt: Datetime!
}
type MailingListSubscriberConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.MailingListSubscriberConnection"
) {
edges: [MailingListSubscriberEdge!]!
pageInfo: PageInfo!
totalCount: Int! @goField(forceResolver: true)
}
type MailingListSubscriberEdge {
cursor: CursorKey!
node: MailingListSubscriber!
}
type Organization implements Node {
id: ID!
name: String!
@@ -3353,6 +3407,17 @@ type Mutation {
deleteTrustCenterAccess(
input: DeleteTrustCenterAccessInput!
): DeleteTrustCenterAccessPayload!
# Mailing List mutations
updateMailingList(
input: UpdateMailingListInput!
): UpdateMailingListPayload!
# Mailing List Subscriber mutations
createMailingListSubscriber(
input: CreateMailingListSubscriberInput!
): CreateMailingListSubscriberPayload!
deleteMailingListSubscriber(
input: DeleteMailingListSubscriberInput!
): DeleteMailingListSubscriberPayload!
# Trust Center Reference mutations
createTrustCenterReference(
input: CreateTrustCenterReferenceInput!
@@ -3755,6 +3820,25 @@ input DeleteTrustCenterAccessInput {
id: ID!
}
input UpdateMailingListInput {
id: ID!
replyTo: EmailAddr
}
type UpdateMailingListPayload {
mailingList: MailingList!
}
input CreateMailingListSubscriberInput {
mailingListId: ID!
fullName: String!
email: EmailAddr!
}
input DeleteMailingListSubscriberInput {
id: ID!
}
input CreateTrustCenterReferenceInput {
trustCenterId: ID!
name: String!
@@ -4606,6 +4690,14 @@ type DeleteTrustCenterAccessPayload {
deletedTrustCenterAccessId: ID!
}
type CreateMailingListSubscriberPayload {
mailingListSubscriberEdge: MailingListSubscriberEdge!
}
type DeleteMailingListSubscriberPayload {
deletedMailingListSubscriberId: ID!
}
type CreateTrustCenterReferencePayload {
trustCenterReferenceEdge: TrustCenterReferenceEdge!
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2025 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"
)
func NewMailingList(ml *coredata.MailingList) *MailingList {
return &MailingList{
ID: ml.ID,
ReplyTo: ml.ReplyTo,
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2025 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 (
MailingListSubscriberOrderBy OrderBy[coredata.MailingListSubscriberOrderField]
MailingListSubscriberConnection struct {
TotalCount int
Edges []*MailingListSubscriberEdge
PageInfo *PageInfo
Resolver any
ParentID gid.GID
}
)
func NewMailingListSubscriber(s *coredata.MailingListSubscriber) *MailingListSubscriber {
return &MailingListSubscriber{
ID: s.ID,
FullName: s.FullName,
Email: s.Email,
Status: s.Status,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}
func NewMailingListSubscriberEdge(s *coredata.MailingListSubscriber, orderBy coredata.MailingListSubscriberOrderField) *MailingListSubscriberEdge {
return &MailingListSubscriberEdge{
Cursor: s.CursorKey(orderBy),
Node: NewMailingListSubscriber(s),
}
}
func NewMailingListSubscriberConnection(
p *page.Page[*coredata.MailingListSubscriber, coredata.MailingListSubscriberOrderField],
resolver any,
mailingListID gid.GID,
) *MailingListSubscriberConnection {
var edges = make([]*MailingListSubscriberEdge, len(p.Data))
for i := range edges {
edges[i] = NewMailingListSubscriberEdge(p.Data[i], p.Cursor.OrderBy.Field)
}
return &MailingListSubscriberConnection{
Edges: edges,
PageInfo: NewPageInfo(p),
Resolver: resolver,
ParentID: mailingListID,
}
}

View File

@@ -15,9 +15,33 @@
package types
import (
"time"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
)
type TrustCenter struct {
ID gid.GID `json:"id"`
Active bool `json:"active"`
LogoFileURL *string `json:"logoFileUrl,omitempty"`
DarkLogoFileURL *string `json:"darkLogoFileUrl,omitempty"`
NdaFileName *string `json:"ndaFileName,omitempty"`
NdaFileURL *string `json:"ndaFileUrl,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Organization *Organization `json:"organization"`
Accesses *TrustCenterAccessConnection `json:"accesses"`
References *TrustCenterReferenceConnection `json:"references"`
ComplianceFrameworks *ComplianceFrameworkConnection `json:"complianceFrameworks"`
ExternalUrls *ComplianceExternalURLConnection `json:"externalUrls"`
MailingList *MailingList `json:"mailingList,omitempty"`
Permission bool `json:"permission"`
}
func (TrustCenter) IsNode() {}
func (t TrustCenter) GetID() gid.GID { return t.ID }
func NewTrustCenter(tc *coredata.TrustCenter, file *coredata.File) *TrustCenter {
var ndaFileName *string
if file != nil {

View File

@@ -436,6 +436,16 @@ type CreateFrameworkPayload struct {
FrameworkEdge *FrameworkEdge `json:"frameworkEdge"`
}
type CreateMailingListSubscriberInput struct {
MailingListID gid.GID `json:"mailingListId"`
FullName string `json:"fullName"`
Email mail.Addr `json:"email"`
}
type CreateMailingListSubscriberPayload struct {
MailingListSubscriberEdge *MailingListSubscriberEdge `json:"mailingListSubscriberEdge"`
}
type CreateMeasureInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
@@ -959,6 +969,14 @@ type DeleteFrameworkPayload struct {
DeletedFrameworkID gid.GID `json:"deletedFrameworkId"`
}
type DeleteMailingListSubscriberInput struct {
ID gid.GID `json:"id"`
}
type DeleteMailingListSubscriberPayload struct {
DeletedMailingListSubscriberID gid.GID `json:"deletedMailingListSubscriberId"`
}
type DeleteMeasureInput struct {
MeasureID gid.GID `json:"measureId"`
}
@@ -1431,6 +1449,32 @@ type ImportMeasurePayload struct {
MeasureEdges []*MeasureEdge `json:"measureEdges"`
}
type MailingList struct {
ID gid.GID `json:"id"`
ReplyTo *mail.Addr `json:"replyTo,omitempty"`
Subscribers *MailingListSubscriberConnection `json:"subscribers"`
}
func (MailingList) IsNode() {}
func (this MailingList) GetID() gid.GID { return this.ID }
type MailingListSubscriber struct {
ID gid.GID `json:"id"`
FullName string `json:"fullName"`
Email mail.Addr `json:"email"`
Status coredata.MailingListSubscriberStatus `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (MailingListSubscriber) IsNode() {}
func (this MailingListSubscriber) GetID() gid.GID { return this.ID }
type MailingListSubscriberEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *MailingListSubscriber `json:"node"`
}
type Measure struct {
ID gid.GID `json:"id"`
Category string `json:"category"`
@@ -1899,26 +1943,6 @@ type TransferImpactAssessmentFilter struct {
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
}
type TrustCenter struct {
ID gid.GID `json:"id"`
Active bool `json:"active"`
LogoFileURL *string `json:"logoFileUrl,omitempty"`
DarkLogoFileURL *string `json:"darkLogoFileUrl,omitempty"`
NdaFileName *string `json:"ndaFileName,omitempty"`
NdaFileURL *string `json:"ndaFileUrl,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Organization *Organization `json:"organization"`
Accesses *TrustCenterAccessConnection `json:"accesses"`
References *TrustCenterReferenceConnection `json:"references"`
ComplianceFrameworks *ComplianceFrameworkConnection `json:"complianceFrameworks"`
ExternalUrls *ComplianceExternalURLConnection `json:"externalUrls"`
Permission bool `json:"permission"`
}
func (TrustCenter) IsNode() {}
func (this TrustCenter) GetID() gid.GID { return this.ID }
type TrustCenterAccess struct {
ID gid.GID `json:"id"`
NdaSignature *ElectronicSignature `json:"ndaSignature,omitempty"`
@@ -2148,6 +2172,15 @@ type UpdateFrameworkPayload struct {
Framework *Framework `json:"framework"`
}
type UpdateMailingListInput struct {
ID gid.GID `json:"id"`
ReplyTo *mail.Addr `json:"replyTo,omitempty"`
}
type UpdateMailingListPayload struct {
MailingList *MailingList `json:"mailingList"`
}
type UpdateMeasureInput struct {
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`

View File

@@ -1524,6 +1524,47 @@ func (r *frameworkConnectionResolver) TotalCount(ctx context.Context, obj *types
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
// Subscribers is the resolver for the subscribers field on MailingList.
func (r *mailingListResolver) Subscribers(ctx context.Context, obj *types.MailingList, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.MailingListSubscriberConnection, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionMailingListSubscriberList); err != nil {
return nil, err
}
pageOrderBy := page.OrderBy[coredata.MailingListSubscriberOrderField]{
Field: coredata.MailingListSubscriberOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
result, err := r.MailmanService(ctx, obj.ID.TenantID()).ListSubscribers(ctx, obj.ID, cursor)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list mailing list subscribers", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewMailingListSubscriberConnection(result, r, obj.ID), nil
}
// TotalCount is the resolver for the totalCount field.
func (r *mailingListSubscriberConnectionResolver) TotalCount(ctx context.Context, obj *types.MailingListSubscriberConnection) (int, error) {
if err := r.authorize(ctx, obj.ParentID, probo.ActionMailingListSubscriberList); err != nil {
return 0, err
}
switch obj.Resolver.(type) {
case *mailingListResolver:
count, err := r.MailmanService(ctx, obj.ParentID.TenantID()).CountSubscribers(ctx, obj.ParentID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count mailing list subscribers", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
}
panic(fmt.Errorf("not implemented: TotalCount for parent type %T", obj.Resolver))
}
// Evidences is the resolver for the evidences field.
func (r *measureResolver) Evidences(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.EvidenceOrderBy) (*types.EvidenceConnection, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionEvidenceList); err != nil {
@@ -1987,6 +2028,56 @@ func (r *mutationResolver) DeleteTrustCenterAccess(ctx context.Context, input ty
}, nil
}
// UpdateMailingList is the resolver for the updateMailingList field.
func (r *mutationResolver) UpdateMailingList(ctx context.Context, input types.UpdateMailingListInput) (*types.UpdateMailingListPayload, error) {
if err := r.authorize(ctx, input.ID, probo.ActionMailingListUpdate); err != nil {
return nil, err
}
ml, err := r.MailmanService(ctx, input.ID.TenantID()).UpdateMailingList(ctx, input.ID, input.ReplyTo)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot update mailing list", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.UpdateMailingListPayload{
MailingList: types.NewMailingList(ml),
}, nil
}
// CreateMailingListSubscriber is the resolver for the createMailingListSubscriber field.
func (r *mutationResolver) CreateMailingListSubscriber(ctx context.Context, input types.CreateMailingListSubscriberInput) (*types.CreateMailingListSubscriberPayload, error) {
if err := r.authorize(ctx, input.MailingListID, probo.ActionMailingListSubscriberCreate); err != nil {
return nil, err
}
subscriber, err := r.MailmanService(ctx, input.MailingListID.TenantID()).CreateSubscriber(ctx, input.MailingListID, input.Email, input.FullName)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot create mailing list subscriber", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.CreateMailingListSubscriberPayload{
MailingListSubscriberEdge: types.NewMailingListSubscriberEdge(subscriber, coredata.MailingListSubscriberOrderFieldCreatedAt),
}, nil
}
// DeleteMailingListSubscriber is the resolver for the deleteMailingListSubscriber field.
func (r *mutationResolver) DeleteMailingListSubscriber(ctx context.Context, input types.DeleteMailingListSubscriberInput) (*types.DeleteMailingListSubscriberPayload, error) {
if err := r.authorize(ctx, input.ID, probo.ActionMailingListSubscriberDelete); err != nil {
return nil, err
}
if err := r.MailmanService(ctx, input.ID.TenantID()).DeleteSubscriber(ctx, input.ID); err != nil {
r.logger.ErrorCtx(ctx, "cannot delete mailing list subscriber", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.DeleteMailingListSubscriberPayload{
DeletedMailingListSubscriberID: input.ID,
}, nil
}
// CreateTrustCenterReference is the resolver for the createTrustCenterReference field.
func (r *mutationResolver) CreateTrustCenterReference(ctx context.Context, input types.CreateTrustCenterReferenceInput) (*types.CreateTrustCenterReferencePayload, error) {
if err := r.authorize(ctx, input.TrustCenterID, probo.ActionTrustCenterReferenceCreate); err != nil {
@@ -7990,6 +8081,31 @@ func (r *trustCenterResolver) ExternalUrls(ctx context.Context, obj *types.Trust
return types.NewComplianceExternalURLConnection(result), nil
}
// MailingList is the resolver for the mailingList field.
func (r *trustCenterResolver) MailingList(ctx context.Context, obj *types.TrustCenter) (*types.MailingList, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionMailingListSubscriberList); err != nil {
return nil, err
}
if obj.MailingList != nil {
return obj.MailingList, nil
}
prb := r.ProboService(ctx, obj.ID.TenantID())
ml, err := prb.TrustCenters.GetMailingList(ctx, obj.ID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get mailing list for trust center", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
if ml == nil {
return nil, nil
}
return types.NewMailingList(ml), nil
}
// Permission is the resolver for the permission field.
func (r *trustCenterResolver) Permission(ctx context.Context, obj *types.TrustCenter, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
@@ -9072,6 +9188,14 @@ func (r *Resolver) FrameworkConnection() schema.FrameworkConnectionResolver {
return &frameworkConnectionResolver{r}
}
// MailingList returns schema.MailingListResolver implementation.
func (r *Resolver) MailingList() schema.MailingListResolver { return &mailingListResolver{r} }
// MailingListSubscriberConnection returns schema.MailingListSubscriberConnectionResolver implementation.
func (r *Resolver) MailingListSubscriberConnection() schema.MailingListSubscriberConnectionResolver {
return &mailingListSubscriberConnectionResolver{r}
}
// Measure returns schema.MeasureResolver implementation.
func (r *Resolver) Measure() schema.MeasureResolver { return &measureResolver{r} }
@@ -9306,6 +9430,8 @@ type evidenceConnectionResolver struct{ *Resolver }
type fileResolver struct{ *Resolver }
type frameworkResolver struct{ *Resolver }
type frameworkConnectionResolver struct{ *Resolver }
type mailingListResolver struct{ *Resolver }
type mailingListSubscriberConnectionResolver struct{ *Resolver }
type measureResolver struct{ *Resolver }
type measureConnectionResolver struct{ *Resolver }
type meetingResolver struct{ *Resolver }