Add employee page

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-11-18 22:56:22 +01:00
parent 2f4cfd7ac0
commit 975d16c623
44 changed files with 5731 additions and 1842 deletions

View File

@@ -94,6 +94,7 @@ enum InvitationStatus
enum MembershipRole @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") {
OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner")
ADMIN @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAdmin")
EMPLOYEE @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee")
VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer")
}
@@ -522,10 +523,6 @@ enum BusinessImpact
enum DocumentVersionOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentVersionOrderField") {
VERSION
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentVersionOrderFieldVersion"
)
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DocumentVersionOrderFieldCreatedAt"
@@ -2035,6 +2032,29 @@ type Document implements Node {
updatedAt: Datetime!
}
type SignableDocument @goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.SignableDocument"
){
id: ID!
title: String!
description: String
documentType: DocumentType!
classification: DocumentClassification!
signed: Boolean! @goField(forceResolver: true)
versions(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: DocumentVersionOrder
filter: DocumentVersionFilter
): DocumentVersionConnection! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
type Meeting implements Node {
id: ID!
name: String!
@@ -2259,6 +2279,17 @@ type Viewer {
before: CursorKey
orderBy: OrganizationOrder
): OrganizationConnection! @goField(forceResolver: true)
signableDocuments(
organizationId: ID!
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: DocumentOrder
): SignableDocumentConnection! @goField(forceResolver: true)
signableDocument(id: ID!): SignableDocument @goField(forceResolver: true)
}
# Connection Types
@@ -2514,6 +2545,22 @@ type EvidenceEdge {
node: Evidence!
}
type SignableDocumentConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.SignableDocumentConnection"
) {
edges: [SignableDocumentEdge!]!
pageInfo: PageInfo!
}
type SignableDocumentEdge
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.SignableDocumentEdge"
) {
cursor: CursorKey!
node: SignableDocument!
}
type DocumentConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.DocumentConnection"
@@ -2914,9 +2961,16 @@ type Mutation {
input: SendSigningNotificationsInput!
): SendSigningNotificationsPayload! cancelSignatureRequest(
input: CancelSignatureRequestInput!
): CancelSignatureRequestPayload! exportDocumentVersionPDF(
): CancelSignatureRequestPayload!
signDocument(
input: SignDocumentInput!
): SignDocumentPayload!
exportDocumentVersionPDF(
input: ExportDocumentVersionPDFInput!
): ExportDocumentVersionPDFPayload!
exportSignableVersionDocumentPDF(
input: ExportSignableDocumentVersionPDFInput!
): ExportSignableDocumentVersionPDFPayload!
createVendorRiskAssessment(
input: CreateVendorRiskAssessmentInput!
): CreateVendorRiskAssessmentPayload!
@@ -3488,6 +3542,10 @@ input ExportDocumentVersionPDFInput {
withSignatures: Boolean!
}
input ExportSignableDocumentVersionPDFInput {
documentVersionId: ID!
}
input DeleteDocumentInput {
documentId: ID!
}
@@ -4072,6 +4130,10 @@ type ExportDocumentVersionPDFPayload {
data: String!
}
type ExportSignableDocumentVersionPDFPayload {
data: String!
}
type UpdateDocumentPayload {
document: Document!
}
@@ -4186,6 +4248,8 @@ type DocumentVersion implements Node @goModel(model: "go.probo.inc/probo/pkg/ser
filter: DocumentVersionSignatureFilter
): DocumentVersionSignatureConnection! @goField(forceResolver: true)
signed: Boolean! @goField(forceResolver: true)
publishedAt: Datetime
createdAt: Datetime!
updatedAt: Datetime!
@@ -4347,6 +4411,14 @@ type CancelSignatureRequestPayload {
deletedDocumentVersionSignatureId: ID!
}
input SignDocumentInput {
documentVersionId: ID!
}
type SignDocumentPayload {
documentVersionSignature: DocumentVersionSignature!
}
type UploadMeasureEvidencePayload {
evidenceEdge: EvidenceEdge!
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
// 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 (
"time"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
SignableDocumentConnection struct {
Edges []*SignableDocumentEdge
PageInfo *PageInfo
}
SignableDocumentEdge struct {
Cursor page.CursorKey
Node *SignableDocument
}
SignableDocument struct {
ID gid.GID
Title string
Description *string
DocumentType coredata.DocumentType
Classification coredata.DocumentClassification
CreatedAt time.Time
UpdatedAt time.Time
}
)
func (SignableDocument) IsNode() {}
func (this SignableDocument) GetID() gid.GID { return this.ID }
func NewSignableDocumentConnection(
p *page.Page[*SignableDocument, coredata.DocumentOrderField],
) *SignableDocumentConnection {
var edges = make([]*SignableDocumentEdge, len(p.Data))
for i := range edges {
edges[i] = NewSignableDocumentEdge(p.Data[i], p.Cursor.OrderBy.Field)
}
return &SignableDocumentConnection{
Edges: edges,
PageInfo: NewPageInfo(p),
}
}
func NewSignableDocumentEdge(document *SignableDocument, orderBy coredata.DocumentOrderField) *SignableDocumentEdge {
return &SignableDocumentEdge{
Cursor: document.CursorKey(orderBy),
Node: document,
}
}
func (d SignableDocument) CursorKey(orderBy coredata.DocumentOrderField) page.CursorKey {
switch orderBy {
case coredata.DocumentOrderFieldCreatedAt:
return page.NewCursorKey(d.ID, d.CreatedAt)
case coredata.DocumentOrderFieldTitle:
return page.NewCursorKey(d.ID, d.Title)
case coredata.DocumentOrderFieldDocumentType:
return page.NewCursorKey(d.ID, d.DocumentType)
}
panic("unsupported order by")
}

View File

@@ -1153,6 +1153,14 @@ type ExportFrameworkPayload struct {
ExportJobID gid.GID `json:"exportJobId"`
}
type ExportSignableDocumentVersionPDFInput struct {
DocumentVersionID gid.GID `json:"documentVersionId"`
}
type ExportSignableDocumentVersionPDFPayload struct {
Data string `json:"data"`
}
type File struct {
ID gid.GID `json:"id"`
MimeType string `json:"mimeType"`
@@ -1680,6 +1688,14 @@ type Session struct {
ExpiresAt time.Time `json:"expiresAt"`
}
type SignDocumentInput struct {
DocumentVersionID gid.GID `json:"documentVersionId"`
}
type SignDocumentPayload struct {
DocumentVersionSignature *DocumentVersionSignature `json:"documentVersionSignature"`
}
type SlackConnection struct {
ID gid.GID `json:"id"`
Channel *string `json:"channel,omitempty"`
@@ -2506,9 +2522,11 @@ type VerifyDomainPayload struct {
}
type Viewer struct {
ID gid.GID `json:"id"`
User *User `json:"user"`
Organizations *OrganizationConnection `json:"organizations"`
ID gid.GID `json:"id"`
User *User `json:"user"`
Organizations *OrganizationConnection `json:"organizations"`
SignableDocuments *SignableDocumentConnection `json:"signableDocuments"`
SignableDocument *SignableDocument `json:"signableDocument,omitempty"`
}
type Role string

View File

@@ -714,7 +714,9 @@ func (r *documentResolver) Versions(ctx context.Context, obj *types.Document, fi
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := prb.Documents.ListVersions(ctx, obj.ID, cursor)
versionFilter := coredata.NewDocumentVersionFilter()
page, err := prb.Documents.ListVersions(ctx, obj.ID, cursor, versionFilter)
if err != nil {
panic(fmt.Errorf("cannot list document versions: %w", err))
}
@@ -785,7 +787,7 @@ func (r *documentConnectionResolver) TotalCount(ctx context.Context, obj *types.
// Document is the resolver for the document field.
func (r *documentVersionResolver) Document(ctx context.Context, obj *types.DocumentVersion) (*types.Document, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionDocument)
r.MustBeAuthorized(ctx, obj.ID, authz.ActionGetDocument)
prb := r.ProboService(ctx, obj.ID.TenantID())
@@ -862,6 +864,24 @@ func (r *documentVersionResolver) Signatures(ctx context.Context, obj *types.Doc
return types.NewDocumentVersionSignatureConnection(page), nil
}
// Signed is the resolver for the signed field.
func (r *documentVersionResolver) Signed(ctx context.Context, obj *types.DocumentVersion) (bool, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionGetSigned)
user := UserFromContext(ctx)
if user == nil {
panic(fmt.Errorf("user not found in context"))
}
prb := r.ProboService(ctx, obj.ID.TenantID())
signed, err := prb.Documents.IsVersionSignedByUserEmail(ctx, obj.ID, user.EmailAddress)
if err != nil {
panic(fmt.Errorf("cannot check if document version is signed: %w", err))
}
return signed, nil
}
// DocumentVersion is the resolver for the documentVersion field.
func (r *documentVersionSignatureResolver) DocumentVersion(ctx context.Context, obj *types.DocumentVersionSignature) (*types.DocumentVersion, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionDocumentVersion)
@@ -3551,6 +3571,31 @@ func (r *mutationResolver) CancelSignatureRequest(ctx context.Context, input typ
}, nil
}
// SignDocument is the resolver for the signDocument field.
func (r *mutationResolver) SignDocument(ctx context.Context, input types.SignDocumentInput) (*types.SignDocumentPayload, error) {
r.MustBeAuthorized(ctx, input.DocumentVersionID, authz.ActionSignDocument)
user := UserFromContext(ctx)
if user == nil {
panic(fmt.Errorf("user not found in context"))
}
prb := r.ProboService(ctx, input.DocumentVersionID.TenantID())
documentVersionSignature, err := prb.Documents.SignDocumentVersionByEmail(ctx, input.DocumentVersionID, user.EmailAddress)
if err != nil {
var errAlreadySigned *coredata.ErrDocumentVersionSignatureAlreadySigned
if errors.As(err, &errAlreadySigned) {
return nil, gqlutils.Conflict(errAlreadySigned)
}
panic(fmt.Errorf("cannot sign document: %w", err))
}
return &types.SignDocumentPayload{
DocumentVersionSignature: types.NewDocumentVersionSignature(documentVersionSignature),
}, nil
}
// ExportDocumentVersionPDF is the resolver for the exportDocumentVersionPDF field.
func (r *mutationResolver) ExportDocumentVersionPDF(ctx context.Context, input types.ExportDocumentVersionPDFInput) (*types.ExportDocumentVersionPDFPayload, error) {
r.MustBeAuthorized(ctx, input.DocumentVersionID, authz.ActionExportDocumentVersionPDF)
@@ -3573,6 +3618,49 @@ func (r *mutationResolver) ExportDocumentVersionPDF(ctx context.Context, input t
}, nil
}
// ExportSignableVersionDocumentPDF is the resolver for the exportSignableVersionDocumentPDF field.
func (r *mutationResolver) ExportSignableVersionDocumentPDF(ctx context.Context, input types.ExportSignableDocumentVersionPDFInput) (*types.ExportSignableDocumentVersionPDFPayload, error) {
r.MustBeAuthorized(ctx, input.DocumentVersionID, authz.ActionExportSignableVersionDocumentPDF)
prb := r.ProboService(ctx, input.DocumentVersionID.TenantID())
documentVersion, err := prb.Documents.GetVersion(ctx, input.DocumentVersionID)
if err != nil {
panic(fmt.Errorf("cannot get document version: %w", err))
}
user := UserFromContext(ctx)
if user == nil {
panic(fmt.Errorf("user not found in context"))
}
documentFilter := coredata.NewDocumentFilter(nil).WithUserEmail(&user.EmailAddress)
_, err = prb.Documents.GetWithFilter(ctx, documentVersion.DocumentID, documentFilter)
if err != nil {
var errNotFound *coredata.ErrDocumentNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get signable document: %w", err))
}
options := probo.ExportPDFOptions{
WithSignatures: false,
WithWatermark: true,
WatermarkEmail: &user.EmailAddress,
}
pdf, err := prb.Documents.ExportPDF(ctx, input.DocumentVersionID, options)
if err != nil {
panic(fmt.Errorf("cannot export signable document PDF: %w", err))
}
return &types.ExportSignableDocumentVersionPDFPayload{
Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)),
}, nil
}
// CreateVendorRiskAssessment is the resolver for the createVendorRiskAssessment field.
func (r *mutationResolver) CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error) {
r.MustBeAuthorized(ctx, input.VendorID, authz.ActionCreateVendorRiskAssessment)
@@ -5994,6 +6082,59 @@ func (r *sAMLConfigurationResolver) TestLoginURL(ctx context.Context, obj *types
return fmt.Sprintf("%s/connect/saml/login/%s", parts[0], obj.ID), nil
}
// Signed is the resolver for the signed field.
func (r *signableDocumentResolver) Signed(ctx context.Context, obj *types.SignableDocument) (bool, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionGetSigned)
user := UserFromContext(ctx)
if user == nil {
panic(fmt.Errorf("user not found in context"))
}
prb := r.ProboService(ctx, obj.ID.TenantID())
signed, err := prb.Documents.IsSigned(ctx, obj.ID, user.EmailAddress)
if err != nil {
panic(fmt.Errorf("cannot check if document is signed: %w", err))
}
return signed, nil
}
// Versions is the resolver for the versions field.
func (r *signableDocumentResolver) Versions(ctx context.Context, obj *types.SignableDocument, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentVersionOrderBy, filter *types.DocumentVersionFilter) (*types.DocumentVersionConnection, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionListSignableDocumentVersion)
prb := r.ProboService(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.DocumentVersionOrderField]{
Field: coredata.DocumentVersionOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.DocumentVersionOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
user := UserFromContext(ctx)
if user == nil {
panic(fmt.Errorf("user not found in context"))
}
versionFilter := coredata.NewDocumentVersionFilter().WithUserEmail(&user.EmailAddress)
page, err := prb.Documents.ListVersions(ctx, obj.ID, cursor, versionFilter)
if err != nil {
panic(fmt.Errorf("cannot list signable document versions: %w", err))
}
return types.NewDocumentVersionConnection(page), nil
}
// Organization is the resolver for the organization field.
func (r *snapshotResolver) Organization(ctx context.Context, obj *types.Snapshot) (*types.Organization, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionGetOrganization)
@@ -6349,7 +6490,7 @@ func (r *trustCenterAccessResolver) AvailableDocumentAccesses(ctx context.Contex
// Document is the resolver for the document field.
func (r *trustCenterDocumentAccessResolver) Document(ctx context.Context, obj *types.TrustCenterDocumentAccess) (*types.Document, error) {
r.MustBeAuthorized(ctx, obj.TrustCenterAccessID, authz.ActionDocument)
r.MustBeAuthorized(ctx, obj.ID, authz.ActionGet)
if obj.DocumentID == nil {
return nil, nil
@@ -6979,6 +7120,85 @@ func (r *viewerResolver) Organizations(ctx context.Context, obj *types.Viewer, f
return types.NewOrganizationConnection(page), nil
}
// SignableDocuments is the resolver for the signableDocuments field.
func (r *viewerResolver) SignableDocuments(ctx context.Context, obj *types.Viewer, organizationID gid.GID, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy) (*types.SignableDocumentConnection, error) {
r.MustBeAuthorized(ctx, organizationID, authz.ActionListSignableDocuments)
prb := r.ProboService(ctx, organizationID.TenantID())
pageOrderBy := page.OrderBy[coredata.DocumentOrderField]{
Field: coredata.DocumentOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.DocumentOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
user := UserFromContext(ctx)
if user == nil {
panic(fmt.Errorf("user not found in context"))
}
documentFilter := coredata.NewDocumentFilter(nil).WithUserEmail(&user.EmailAddress)
documentsPage, err := prb.Documents.ListByOrganizationID(ctx, organizationID, cursor, documentFilter)
if err != nil {
panic(fmt.Errorf("cannot list organization signable documents: %w", err))
}
signableDocuments := make([]*types.SignableDocument, len(documentsPage.Data))
for i, doc := range documentsPage.Data {
signableDocuments[i] = &types.SignableDocument{
ID: doc.ID,
Title: doc.Title,
DocumentType: doc.DocumentType,
Classification: doc.Classification,
CreatedAt: doc.CreatedAt,
UpdatedAt: doc.UpdatedAt,
}
}
page := page.NewPage(signableDocuments, documentsPage.Cursor)
return types.NewSignableDocumentConnection(page), nil
}
// SignableDocument is the resolver for the signableDocument field.
func (r *viewerResolver) SignableDocument(ctx context.Context, obj *types.Viewer, id gid.GID) (*types.SignableDocument, error) {
r.MustBeAuthorized(ctx, id, authz.ActionGetSignableDocument)
prb := r.ProboService(ctx, id.TenantID())
user := UserFromContext(ctx)
if user == nil {
panic(fmt.Errorf("user not found in context"))
}
documentFilter := coredata.NewDocumentFilter(nil).WithUserEmail(&user.EmailAddress)
document, err := prb.Documents.GetWithFilter(ctx, id, documentFilter)
if err != nil {
var errNotFound *coredata.ErrDocumentNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get signable document: %w", err))
}
return &types.SignableDocument{
ID: document.ID,
Title: document.Title,
DocumentType: document.DocumentType,
Classification: document.Classification,
CreatedAt: document.CreatedAt,
UpdatedAt: document.UpdatedAt,
}, nil
}
// Asset returns schema.AssetResolver implementation.
func (r *Resolver) Asset() schema.AssetResolver { return &assetResolver{r} }
@@ -7144,6 +7364,11 @@ func (r *Resolver) SAMLConfiguration() schema.SAMLConfigurationResolver {
return &sAMLConfigurationResolver{r}
}
// SignableDocument returns schema.SignableDocumentResolver implementation.
func (r *Resolver) SignableDocument() schema.SignableDocumentResolver {
return &signableDocumentResolver{r}
}
// Snapshot returns schema.SnapshotResolver implementation.
func (r *Resolver) Snapshot() schema.SnapshotResolver { return &snapshotResolver{r} }
@@ -7277,6 +7502,7 @@ type reportResolver struct{ *Resolver }
type riskResolver struct{ *Resolver }
type riskConnectionResolver struct{ *Resolver }
type sAMLConfigurationResolver struct{ *Resolver }
type signableDocumentResolver struct{ *Resolver }
type snapshotResolver struct{ *Resolver }
type snapshotConnectionResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }

View File

@@ -1595,7 +1595,7 @@ func (r *Resolver) ListDocumentVersionsTool(ctx context.Context, req *mcp.CallTo
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
svc := r.ProboService(ctx, input.DocumentID)
page, err := svc.Documents.ListVersions(ctx, input.DocumentID, cursor)
page, err := svc.Documents.ListVersions(ctx, input.DocumentID, cursor, coredata.NewDocumentVersionFilter())
if err != nil {
panic(fmt.Errorf("cannot list document versions: %w", err))
}