Add electronic signature

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-02-16 23:12:09 +01:00
parent e6f9d7aae2
commit c191d25e9a
58 changed files with 7557 additions and 292 deletions

View File

@@ -18,6 +18,7 @@ import (
"net/http"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/server/api/authz"
@@ -25,12 +26,13 @@ import (
"go.probo.inc/probo/pkg/server/gqlutils"
)
func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, customDomainCname string, logger *log.Logger) http.Handler {
func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, esignSvc *esign.Service, customDomainCname string, logger *log.Logger) http.Handler {
config := schema.Config{
Resolvers: &Resolver{
authorize: authz.NewAuthorizeFunc(iamSvc, logger),
probo: proboSvc,
iam: iamSvc,
esign: esignSvc,
customDomainCname: customDomainCname,
logger: logger,
},

View File

@@ -31,6 +31,7 @@ import (
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/connector"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/probo"
@@ -47,6 +48,7 @@ type (
authorize authz.AuthorizeFunc
probo *probo.Service
iam *iam.Service
esign *esign.Service
logger *log.Logger
customDomainCname string
}
@@ -56,6 +58,7 @@ func NewMux(
logger *log.Logger,
proboSvc *probo.Service,
iamSvc *iam.Service,
esignSvc *esign.Service,
cookieConfig securecookie.Config,
tokenSecret string,
connectorRegistry *connector.ConnectorRegistry,
@@ -66,7 +69,7 @@ func NewMux(
safeRedirect := &saferedirect.SafeRedirect{AllowedHost: baseURL.Host()}
graphqlHandler := NewGraphQLHandler(iamSvc, proboSvc, customDomainCname, logger)
graphqlHandler := NewGraphQLHandler(iamSvc, proboSvc, esignSvc, customDomainCname, logger)
r.Group(func(r chi.Router) {
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))

View File

@@ -2714,6 +2714,7 @@ type TrustCenterAccess implements Node {
name: String!
state: TrustCenterAccessState!
hasAcceptedNonDisclosureAgreement: Boolean!
ndaSignature: ElectronicSignature @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
lastTokenExpiresAt: Datetime
@@ -5387,3 +5388,149 @@ type CreateCustomDomainPayload {
type DeleteCustomDomainPayload {
deletedCustomDomainId: ID!
}
# Electronic Signature
enum ElectronicSignatureStatus
@goModel(
model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatus"
) {
PENDING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusPending"
)
ACCEPTED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusAccepted"
)
PROCESSING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusProcessing"
)
COMPLETED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusCompleted"
)
FAILED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusFailed"
)
}
enum ElectronicSignatureDocumentType
@goModel(
model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentType"
) {
NDA
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeNDA"
)
DPA
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeDPA"
)
MSA
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeMSA"
)
SOW
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeSOW"
)
SLA
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeSLA"
)
TOS
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeTOS"
)
PRIVACY_POLICY
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypePrivacyPolicy"
)
OTHER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeOther"
)
}
enum ElectronicSignatureEventType
@goModel(
model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventType"
) {
DOCUMENT_VIEWED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeDocumentViewed"
)
CONSENT_GIVEN
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeConsentGiven"
)
FULL_NAME_TYPED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeFullNameTyped"
)
SIGNATURE_ACCEPTED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSignatureAccepted"
)
SIGNATURE_COMPLETED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSignatureCompleted"
)
SEAL_COMPUTED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSealComputed"
)
TIMESTAMP_REQUESTED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeTimestampRequested"
)
CERTIFICATE_GENERATED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeCertificateGenerated"
)
PROCESSING_ERROR
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeProcessingError"
)
}
enum ElectronicSignatureEventSource
@goModel(
model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventSource"
) {
CLIENT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventSourceClient"
)
SERVER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventSourceServer"
)
}
type ElectronicSignature implements Node {
id: ID!
status: ElectronicSignatureStatus!
documentType: ElectronicSignatureDocumentType!
consentText: String!
lastError: String
signedAt: Datetime
certificateFileUrl: String @goField(forceResolver: true)
events: [ElectronicSignatureEvent!]! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
type ElectronicSignatureEvent {
id: ID!
eventType: ElectronicSignatureEventType!
eventSource: ElectronicSignatureEventSource!
actorEmail: String!
actorIpAddress: String!
actorUserAgent: String!
occurredAt: Datetime!
createdAt: Datetime!
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
// 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 NewElectronicSignature(es *coredata.ElectronicSignature) *ElectronicSignature {
return &ElectronicSignature{
ID: es.ID,
Status: es.Status,
DocumentType: es.DocumentType,
ConsentText: es.ConsentText,
LastError: es.LastError,
SignedAt: es.SignedAt,
CreatedAt: es.CreatedAt,
UpdatedAt: es.UpdatedAt,
}
}
func NewElectronicSignatureEvent(ev *coredata.ElectronicSignatureEvent) *ElectronicSignatureEvent {
return &ElectronicSignatureEvent{
ID: ev.ID,
EventType: ev.EventType,
EventSource: ev.EventSource,
ActorEmail: ev.ActorEmail,
ActorIPAddress: ev.ActorIPAddress,
ActorUserAgent: ev.ActorUserAgent,
OccurredAt: ev.OccurredAt,
CreatedAt: ev.CreatedAt,
}
}

View File

@@ -1206,6 +1206,33 @@ type DocumentVersionSignatureOrder struct {
Direction page.OrderDirection `json:"direction"`
}
type ElectronicSignature struct {
ID gid.GID `json:"id"`
Status coredata.ElectronicSignatureStatus `json:"status"`
DocumentType coredata.ElectronicSignatureDocumentType `json:"documentType"`
ConsentText string `json:"consentText"`
LastError *string `json:"lastError,omitempty"`
SignedAt *time.Time `json:"signedAt,omitempty"`
CertificateFileURL *string `json:"certificateFileUrl,omitempty"`
Events []*ElectronicSignatureEvent `json:"events"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (ElectronicSignature) IsNode() {}
func (this ElectronicSignature) GetID() gid.GID { return this.ID }
type ElectronicSignatureEvent struct {
ID gid.GID `json:"id"`
EventType coredata.ElectronicSignatureEventType `json:"eventType"`
EventSource coredata.ElectronicSignatureEventSource `json:"eventSource"`
ActorEmail string `json:"actorEmail"`
ActorIPAddress string `json:"actorIpAddress"`
ActorUserAgent string `json:"actorUserAgent"`
OccurredAt time.Time `json:"occurredAt"`
CreatedAt time.Time `json:"createdAt"`
}
type Evidence struct {
ID gid.GID `json:"id"`
Size int `json:"size"`
@@ -1847,6 +1874,7 @@ type TrustCenterAccess struct {
Name string `json:"name"`
State coredata.TrustCenterAccessState `json:"state"`
HasAcceptedNonDisclosureAgreement bool `json:"hasAcceptedNonDisclosureAgreement"`
NdaSignature *ElectronicSignature `json:"ndaSignature,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
LastTokenExpiresAt *time.Time `json:"lastTokenExpiresAt,omitempty"`

View File

@@ -1241,6 +1241,40 @@ func (r *documentVersionSignatureConnectionResolver) TotalCount(ctx context.Cont
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
// CertificateFileURL is the resolver for the certificateFileUrl field.
func (r *electronicSignatureResolver) CertificateFileURL(ctx context.Context, obj *types.ElectronicSignature) (*string, error) {
sig, err := r.esign.LoadSignatureByID(ctx, obj.ID)
if err != nil {
return nil, fmt.Errorf("cannot load signature: %w", err)
}
if sig.CertificateFileID == nil {
return nil, nil
}
url, err := r.esign.GenerateCertificateFileURL(ctx, *sig.CertificateFileID, 1*time.Hour)
if err != nil {
return nil, fmt.Errorf("cannot generate certificate file URL: %w", err)
}
return &url, nil
}
// Events is the resolver for the events field.
func (r *electronicSignatureResolver) Events(ctx context.Context, obj *types.ElectronicSignature) ([]*types.ElectronicSignatureEvent, error) {
events, err := r.esign.LoadEventsBySignatureID(ctx, obj.ID)
if err != nil {
return nil, fmt.Errorf("cannot load signature events: %w", err)
}
result := make([]*types.ElectronicSignatureEvent, len(events))
for i := range events {
result[i] = types.NewElectronicSignatureEvent(events[i])
}
return result, nil
}
// File is the resolver for the file field.
func (r *evidenceResolver) File(ctx context.Context, obj *types.Evidence) (*types.File, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionFileGet); err != nil {
@@ -7785,6 +7819,42 @@ func (r *trustCenterResolver) Permission(ctx context.Context, obj *types.TrustCe
return r.Resolver.Permission(ctx, obj, action)
}
// NdaSignature is the resolver for the ndaSignature field.
func (r *trustCenterAccessResolver) NdaSignature(ctx context.Context, obj *types.TrustCenterAccess) (*types.ElectronicSignature, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionTrustCenterAccessGet); err != nil {
return nil, err
}
prb := r.ProboService(ctx, obj.ID.TenantID())
access, err := prb.TrustCenterAccesses.Get(ctx, obj.ID)
if err != nil {
return nil, fmt.Errorf("cannot load trust center access: %w", err)
}
trustCenter, _, err := prb.TrustCenters.Get(ctx, access.TrustCenterID)
if err != nil {
return nil, fmt.Errorf("cannot load trust center: %w", err)
}
if trustCenter.NonDisclosureAgreementFileID == nil {
return nil, nil
}
sig, err := r.esign.LoadSignatureByOrgEmailAndDocType(
ctx,
access.OrganizationID,
string(access.Email),
coredata.ElectronicSignatureDocumentTypeNDA,
*trustCenter.NonDisclosureAgreementFileID,
)
if err != nil {
return nil, nil // No signature row — pre-existing access
}
return types.NewElectronicSignature(sig), nil
}
// PendingRequestCount is the resolver for the pendingRequestCount field.
func (r *trustCenterAccessResolver) PendingRequestCount(ctx context.Context, obj *types.TrustCenterAccess) (int, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionTrustCenterAccessGet); err != nil {
@@ -8784,6 +8854,11 @@ func (r *Resolver) DocumentVersionSignatureConnection() schema.DocumentVersionSi
return &documentVersionSignatureConnectionResolver{r}
}
// ElectronicSignature returns schema.ElectronicSignatureResolver implementation.
func (r *Resolver) ElectronicSignature() schema.ElectronicSignatureResolver {
return &electronicSignatureResolver{r}
}
// Evidence returns schema.EvidenceResolver implementation.
func (r *Resolver) Evidence() schema.EvidenceResolver { return &evidenceResolver{r} }
@@ -9029,6 +9104,7 @@ type documentVersionResolver struct{ *Resolver }
type documentVersionConnectionResolver struct{ *Resolver }
type documentVersionSignatureResolver struct{ *Resolver }
type documentVersionSignatureConnectionResolver struct{ *Resolver }
type electronicSignatureResolver struct{ *Resolver }
type evidenceResolver struct{ *Resolver }
type evidenceConnectionResolver struct{ *Resolver }
type fileResolver struct{ *Resolver }