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

@@ -25,6 +25,7 @@ import (
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/connector"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/securecookie"
@@ -44,6 +45,7 @@ type (
Probo *probo.Service
IAM *iam.Service
Trust *trust.Service
ESign *esign.Service
Slack *slack.Service
Cookie securecookie.Config
TokenSecret string
@@ -117,6 +119,7 @@ func NewServer(cfg Config) (*Server, error) {
cfg.Logger.Named("trust.v1"),
cfg.IAM,
cfg.Trust,
cfg.ESign,
cfg.Cookie,
cfg.BaseURL,
),
@@ -124,6 +127,7 @@ func NewServer(cfg Config) (*Server, error) {
cfg.Logger.Named("console.v1"),
cfg.Probo,
cfg.IAM,
cfg.ESign,
cfg.Cookie,
cfg.TokenSecret,
cfg.ConnectorRegistry,

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 }

View File

@@ -19,6 +19,7 @@ import (
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/authn"
@@ -28,11 +29,12 @@ import (
"go.probo.inc/probo/pkg/trust"
)
func NewGraphQLHandler(iamSvc *iam.Service, trustSvc *trust.Service, logger *log.Logger, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config) http.Handler {
func NewGraphQLHandler(iamSvc *iam.Service, trustSvc *trust.Service, esignSvc *esign.Service, logger *log.Logger, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config) http.Handler {
config := schema.Config{
Resolvers: &Resolver{
iam: iamSvc,
trust: trustSvc,
esign: esignSvc,
logger: logger,
baseURL: baseURL,
sessionCookie: authn.NewCookie(&cookieConfig),

View File

@@ -23,6 +23,7 @@ import (
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/securecookie"
@@ -46,6 +47,7 @@ type (
Resolver struct {
trust *trust.Service
esign *esign.Service
logger *log.Logger
iam *iam.Service
sessionCookie *authn.Cookie
@@ -75,6 +77,7 @@ func NewMux(
logger *log.Logger,
iamSvc *iam.Service,
trustSvc *trust.Service,
esignSvc *esign.Service,
cookieConfig securecookie.Config,
baseURL *baseurl.BaseURL,
) *chi.Mux {
@@ -84,7 +87,7 @@ func NewMux(
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
r.Use(compliancepage.NewMembershipMiddleware(trustSvc, logger))
graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, logger, baseURL, cookieConfig)
graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, esignSvc, logger, baseURL, cookieConfig)
r.Handle("/graphql", graphqlHandler)

View File

@@ -496,6 +496,7 @@ type TrustCenter implements Node {
darkLogoFileUrl: String @goField(forceResolver: true)
ndaFileName: String
ndaFileUrl: String @goField(forceResolver: true)
ndaSignature: ElectronicSignature @goField(forceResolver: true)
organization: Organization! @goField(forceResolver: true)
isViewerMember: Boolean! @goField(forceResolver: true)
hasAcceptedNonDisclosureAgreement: Boolean! @goField(forceResolver: true)
@@ -608,6 +609,143 @@ type AcceptNonDisclosureAgreementPayload {
success: Boolean!
}
# 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"
)
}
type ElectronicSignature implements Node {
id: ID!
status: ElectronicSignatureStatus!
documentType: ElectronicSignatureDocumentType!
consentText: String!
lastError: String
signedAt: Datetime
createdAt: Datetime!
updatedAt: Datetime!
}
input AcceptElectronicSignatureInput {
signatureId: ID!
fullName: String!
}
type AcceptElectronicSignaturePayload {
signature: ElectronicSignature!
}
input RecordSigningEventInput {
signatureId: ID!
eventType: ElectronicSignatureEventType!
}
type RecordSigningEventPayload {
success: Boolean!
}
type Query {
viewer: Identity
node(id: ID!): Node!
@@ -647,4 +785,12 @@ type Mutation {
requestTrustCenterFileAccess(
input: RequestTrustCenterFileAccessInput!
): RequestAccessesPayload! @session(required: PRESENT)
acceptElectronicSignature(
input: AcceptElectronicSignatureInput!
): AcceptElectronicSignaturePayload @session(required: PRESENT)
recordSigningEvent(
input: RecordSigningEventInput!
): RecordSigningEventPayload @session(required: PRESENT)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
// 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,
}
}

View File

@@ -16,6 +16,15 @@ type Node interface {
GetID() gid.GID
}
type AcceptElectronicSignatureInput struct {
SignatureID gid.GID `json:"signatureId"`
FullName string `json:"fullName"`
}
type AcceptElectronicSignaturePayload struct {
Signature *ElectronicSignature `json:"signature"`
}
type AcceptNonDisclosureAgreementInput struct {
FullName string `json:"fullName"`
}
@@ -64,6 +73,20 @@ type DocumentEdge struct {
Node *Document `json:"node"`
}
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"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (ElectronicSignature) IsNode() {}
func (this ElectronicSignature) GetID() gid.GID { return this.ID }
type ExportDocumentPDFInput struct {
DocumentID gid.GID `json:"documentId"`
}
@@ -136,6 +159,15 @@ type PageInfo struct {
type Query struct {
}
type RecordSigningEventInput struct {
SignatureID gid.GID `json:"signatureId"`
EventType coredata.ElectronicSignatureEventType `json:"eventType"`
}
type RecordSigningEventPayload struct {
Success bool `json:"success"`
}
type Report struct {
ID gid.GID `json:"id"`
Filename string `json:"filename"`
@@ -178,6 +210,7 @@ type TrustCenter struct {
DarkLogoFileURL *string `json:"darkLogoFileUrl,omitempty"`
NdaFileName *string `json:"ndaFileName,omitempty"`
NdaFileURL *string `json:"ndaFileUrl,omitempty"`
NdaSignature *ElectronicSignature `json:"ndaSignature,omitempty"`
Organization *Organization `json:"organization"`
IsViewerMember bool `json:"isViewerMember"`
HasAcceptedNonDisclosureAgreement bool `json:"hasAcceptedNonDisclosureAgreement"`

View File

@@ -14,6 +14,7 @@ import (
"go.gearno.de/kit/log"
"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/page"
@@ -648,6 +649,71 @@ func (r *mutationResolver) RequestTrustCenterFileAccess(ctx context.Context, inp
}, nil
}
// AcceptElectronicSignature is the resolver for the acceptElectronicSignature field.
func (r *mutationResolver) AcceptElectronicSignature(ctx context.Context, input types.AcceptElectronicSignatureInput) (*types.AcceptElectronicSignaturePayload, error) {
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated")
}
httpReq := gqlutils.HTTPRequestFromContext(ctx)
if _, err := r.iam.AuthService.UpdateIdentity(ctx, identity.ID, input.FullName); err != nil {
var errNotFound *iam.ErrIdentityNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFound(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot update identity", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
if err := r.esign.AcceptSignature(ctx, &esign.AcceptSignatureRequest{
SignatureID: input.SignatureID,
SignerFullName: input.FullName,
SignerEmail: identity.EmailAddress,
SignerIPAddr: httpReq.RemoteAddr,
SignerUA: httpReq.UserAgent(),
}); err != nil {
r.logger.ErrorCtx(ctx, "cannot accept electronic signature", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
sig, err := r.esign.LoadSignatureByID(ctx, input.SignatureID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot load electronic signature", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.AcceptElectronicSignaturePayload{
Signature: types.NewElectronicSignature(sig),
}, nil
}
// RecordSigningEvent is the resolver for the recordSigningEvent field.
func (r *mutationResolver) RecordSigningEvent(ctx context.Context, input types.RecordSigningEventInput) (*types.RecordSigningEventPayload, error) {
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated")
}
httpReq := gqlutils.HTTPRequestFromContext(ctx)
if err := r.esign.RecordEvent(ctx, &esign.RecordEventRequest{
SignatureID: input.SignatureID,
EventType: input.EventType,
EventSource: coredata.ElectronicSignatureEventSourceClient,
ActorEmail: identity.EmailAddress,
ActorIPAddr: httpReq.RemoteAddr,
ActorUA: httpReq.UserAgent(),
}); err != nil {
r.logger.ErrorCtx(ctx, "cannot record signing event", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.RecordSigningEventPayload{Success: true}, nil
}
// LogoURL is the resolver for the logoUrl field.
func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
trustService := r.TrustService(ctx, obj.ID.TenantID())
@@ -868,6 +934,32 @@ func (r *trustCenterResolver) NdaFileURL(ctx context.Context, obj *types.TrustCe
return &fileURL, nil
}
// NdaSignature is the resolver for the ndaSignature field.
func (r *trustCenterResolver) NdaSignature(ctx context.Context, obj *types.TrustCenter) (*types.ElectronicSignature, error) {
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return nil, nil
}
trustCenter := compliancepage.CompliancePageFromContext(ctx)
if trustCenter == nil || trustCenter.NonDisclosureAgreementFileID == nil {
return nil, nil
}
sig, err := r.esign.LoadSignatureByOrgEmailAndDocType(
ctx,
trustCenter.OrganizationID,
identity.EmailAddress.String(),
coredata.ElectronicSignatureDocumentTypeNDA,
*trustCenter.NonDisclosureAgreementFileID,
)
if err != nil {
return nil, nil
}
return types.NewElectronicSignature(sig), nil
}
// Organization is the resolver for the organization field.
func (r *trustCenterResolver) Organization(ctx context.Context, obj *types.TrustCenter) (*types.Organization, error) {
return obj.Organization, nil