Move trust GraphQL API under complianceportal v1

Relocate the public trust center GraphQL surface, OAuth handlers,
and SPA serving into the compliance portal API package and remove
the legacy trust v1 server tree.

Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
Bryan Frimin
2026-07-15 10:59:08 +02:00
parent 5b3c33831f
commit 31157ff2e3
48 changed files with 862 additions and 224 deletions

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal
import (
portal "go.probo.inc/probo/pkg/complianceportal"
)
const (
VisitorOAuthScope = portal.VisitorOAuthScope
GraphQLPath = "/graphql"
CIMDMetadataPath = portal.CIMDMetadataPath
OAuthInitiatePath = "/initiate"
OAuthCallbackPath = portal.OAuthCallbackPath
)
func CIMDClientIDURL(portalBaseURL string) (string, error) {
return portal.CIMDClientIDURL(portalBaseURL)
}
func OAuthCallbackURL(portalBaseURL string) (string, error) {
return portal.OAuthCallbackURL(portalBaseURL)
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal
import (
"net/http"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/authn"
)
func TrustedRequestHost(r *http.Request) (string, bool) {
if r.TLS != nil && r.TLS.ServerName != "" {
return coredata.NormalizeBoundHost(r.TLS.ServerName), true
}
if r.Host == "" {
return "", false
}
return coredata.NormalizeBoundHost(r.Host), true
}
func NewSessionHostMiddleware(cookieConfig securecookie.Config) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session := authn.SessionFromContext(ctx)
if session == nil {
next.ServeHTTP(w, r)
return
}
host, ok := TrustedRequestHost(r)
if ok && session.Data.MatchesBoundHost(host) {
next.ServeHTTP(w, r)
return
}
securecookie.Clear(w, cookieConfig)
ctx = authn.ContextWithSession(ctx, nil)
ctx = authn.ContextWithIdentity(ctx, nil)
next.ServeHTTP(w, r.WithContext(ctx))
},
)
}
}

View File

@@ -0,0 +1,135 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal
import (
"crypto/tls"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/authn"
)
func TestTrustedRequestHost_PrefersTLSServerName(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "https://evil.example.com/graphql", nil)
req.Host = "evil.example.com"
req.TLS = &tls.ConnectionState{ServerName: "portal.example.com"}
host, ok := TrustedRequestHost(req)
require.True(t, ok)
assert.Equal(t, "portal.example.com", host)
}
func TestSessionHostMiddleware_RejectsMismatchedHost(t *testing.T) {
t.Parallel()
var authenticated bool
handler := NewSessionHostMiddleware(securecookie.Config{Name: "ssid"})(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authenticated = authn.IdentityFromContext(r.Context()) != nil
w.WriteHeader(http.StatusOK)
}),
)
identity := &coredata.Identity{ID: gid.New(gid.NilTenant, coredata.IdentityEntityType)}
session := &coredata.Session{
ID: identity.ID,
Data: coredata.SessionDataForHost("portal-a.example.com"),
}
req := httptest.NewRequest(http.MethodGet, "/graphql", nil)
req.Host = "portal-b.example.com"
req.TLS = &tls.ConnectionState{ServerName: "portal-b.example.com"}
req = req.WithContext(authn.ContextWithIdentity(req.Context(), identity))
req = req.WithContext(authn.ContextWithSession(req.Context(), session))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
assert.False(t, authenticated)
}
func TestSessionHostMiddleware_AllowsMatchingTLSHost(t *testing.T) {
t.Parallel()
var authenticated bool
handler := NewSessionHostMiddleware(securecookie.Config{Name: "ssid"})(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authenticated = authn.IdentityFromContext(r.Context()) != nil
w.WriteHeader(http.StatusOK)
}),
)
identity := &coredata.Identity{ID: gid.New(gid.NilTenant, coredata.IdentityEntityType)}
session := &coredata.Session{
ID: identity.ID,
Data: coredata.SessionDataForHost("portal.example.com"),
}
req := httptest.NewRequest(http.MethodGet, "/graphql", nil)
req.Host = "evil.example.com"
req.TLS = &tls.ConnectionState{ServerName: "portal.example.com"}
req = req.WithContext(authn.ContextWithIdentity(req.Context(), identity))
req = req.WithContext(authn.ContextWithSession(req.Context(), session))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
assert.True(t, authenticated)
}
func TestSessionHostMiddleware_RejectsSpoofedHostHeader(t *testing.T) {
t.Parallel()
var authenticated bool
handler := NewSessionHostMiddleware(securecookie.Config{Name: "ssid"})(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authenticated = authn.IdentityFromContext(r.Context()) != nil
w.WriteHeader(http.StatusOK)
}),
)
identity := &coredata.Identity{ID: gid.New(gid.NilTenant, coredata.IdentityEntityType)}
session := &coredata.Session{
ID: identity.ID,
Data: coredata.SessionDataForHost("portal.example.com"),
}
req := httptest.NewRequest(http.MethodGet, "/graphql", nil)
req.Host = "portal.example.com"
req.TLS = &tls.ConnectionState{ServerName: "other.example.com"}
req = req.WithContext(authn.ContextWithIdentity(req.Context(), identity))
req = req.WithContext(authn.ContextWithSession(req.Context(), session))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
assert.False(t, authenticated)
}

View File

@@ -1,4 +1,4 @@
package trust_v1
package complianceportal_v1
// This file will be automatically regenerated based on the schema, any resolver
// implementations
@@ -16,7 +16,7 @@ import (
"go.probo.inc/probo/pkg/saferedirect"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/complianceportal"
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
)
@@ -123,6 +123,22 @@ func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.Veri
}
}
req := gqlutils.HTTPRequestFromContext(ctx)
if req == nil {
return nil, gqlutils.Internal(ctx)
}
host, ok := complianceportal.TrustedRequestHost(req)
if !ok {
return nil, gqlutils.Internal(ctx)
}
session.Data = coredata.SessionDataForHost(host)
if err := r.iam.SessionService.UpdateSessionData(ctx, session.ID, session.Data); err != nil {
r.logger.ErrorCtx(ctx, "cannot bind session to host", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
trustCenter := complianceportal.CompliancePageFromContext(ctx)
if _, err := r.trust.ProvisionPortalMember(ctx, trustCenter.ID, identity.ID); err != nil {

View File

@@ -1,4 +1,4 @@
package trust_v1
package complianceportal_v1
// This file will be automatically regenerated based on the schema, any resolver
// implementations
@@ -17,8 +17,8 @@ import (
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/complianceportal"
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/schema"
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
)

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package trust_v1
package complianceportal_v1
import (
"context"
@@ -27,7 +27,7 @@ import (
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
)

View File

@@ -14,7 +14,7 @@ model:
resolver:
layout: "follow-schema"
dir: "."
package: "trust_v1"
package: "complianceportal_v1"
filename_template: "{name}_resolvers.go"
autobind: []

View File

@@ -29,10 +29,7 @@ enum MailingListSubscriberStatus
)
}
type MailingListSubscriber implements Node
@goModel(
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.MailingListSubscriber"
) {
type MailingListSubscriber implements Node {
id: ID!
fullName: String!
email: EmailAddr!

View File

@@ -172,26 +172,20 @@ type AuditEdge @nda {
node: Audit!
}
type ComplianceFramework implements Node
@goModel(
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.ComplianceFramework"
) {
type ComplianceFramework implements Node {
id: ID!
framework: Framework! @goField(forceResolver: true)
}
type ComplianceFrameworkConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.ComplianceFrameworkConnection"
model: "go.probo.inc/probo/pkg/server/api/complianceportal/v1/types.ComplianceFrameworkConnection"
) {
edges: [ComplianceFrameworkEdge!]!
pageInfo: PageInfo!
}
type ComplianceFrameworkEdge
@goModel(
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.ComplianceFrameworkEdge"
) {
type ComplianceFrameworkEdge {
cursor: CursorKey!
node: ComplianceFramework!
}
@@ -285,7 +279,7 @@ input SubprocessorFilter {
type SubprocessorConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.SubprocessorConnection"
model: "go.probo.inc/probo/pkg/server/api/complianceportal/v1/types.SubprocessorConnection"
) @nda {
edges: [SubprocessorEdge!]!
pageInfo: PageInfo!

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package trust_v1
package complianceportal_v1
import (
"net/http"
@@ -33,7 +33,7 @@ import (
"go.probo.inc/probo/pkg/resourcealias"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/schema"
"go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/server/gqlutils/directives/authentication"
"go.probo.inc/probo/pkg/server/gqlutils/directives/session"

View File

@@ -1,4 +1,4 @@
package trust_v1
package complianceportal_v1
// This file will be automatically regenerated based on the schema, any resolver
// implementations
@@ -13,7 +13,7 @@ import (
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/complianceportal"
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/validator"
)

View File

@@ -0,0 +1,163 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal_v1
import (
"context"
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
"go.gearno.de/x/ref"
"go.probo.inc/probo/pkg/baseurl"
visitor "go.probo.inc/probo/pkg/complianceportal/visitor"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/filemanager"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/resourcealias"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/complianceportal"
"go.probo.inc/probo/pkg/server/gqlutils"
)
type MuxConfig struct {
BaseURL *baseurl.BaseURL
ExtraHeaderFields map[string]string
Logger *log.Logger
IAM *iam.Service
Visitor *visitor.Service
ResourceAlias *resourcealias.Service
File *filemanager.Service
ESign *esign.Service
Mailman *mailman.Service
Cookie securecookie.Config
TokenSecret string
GraphQLLimits gqlutils.Limits
}
func NewMux(cfg MuxConfig) (http.Handler, error) {
webServer, err := NewServer(compliancePageHeadData(cfg.BaseURL))
if err != nil {
return nil, err
}
r := chi.NewRouter()
r.Use(complianceportal.NewSNIMiddleware(cfg.Visitor))
r.Use(server.NewSecurityHeadersMiddleware(cfg.ExtraHeaderFields))
markdownHandler := complianceportal.NewHandler(cfg.Visitor)
r.Get("/llms.txt", markdownHandler.HandleLLMsTxt)
r.Get("/robots.txt", markdownHandler.HandleRobotsTxt)
r.Get("/sitemap.xml", markdownHandler.HandleSitemap)
allowedHost := func(ctx context.Context, host string) bool {
_, err := cfg.Visitor.GetPortalByDomainName(ctx, host)
return err == nil
}
oauthInitiateHandler := NewOAuthInitiateHandler(
cfg.BaseURL,
cfg.Visitor,
allowedHost,
cfg.Logger,
)
oauthCallbackHandler := NewOAuthCallbackHandler(
cfg.IAM,
cfg.Visitor,
cfg.Cookie,
allowedHost,
cfg.Logger,
)
graphqlHandler := NewGraphQLHandler(
cfg.IAM,
cfg.Visitor,
cfg.ResourceAlias,
cfg.File,
cfg.ESign,
cfg.Mailman,
cfg.Logger,
cfg.BaseURL,
cfg.Cookie,
cfg.TokenSecret,
cfg.GraphQLLimits,
)
r.Group(
func(r chi.Router) {
r.Use(complianceportal.NewCompliancePagePresenceMiddleware())
r.Method(http.MethodGet, complianceportal.CIMDMetadataPath, NewOAuthClientMetadataHandler())
r.Method(http.MethodGet, complianceportal.OAuthInitiatePath, oauthInitiateHandler)
r.Method(http.MethodGet, complianceportal.OAuthCallbackPath, oauthCallbackHandler)
r.Group(
func(r chi.Router) {
r.Use(authn.NewSessionMiddleware(cfg.IAM, cfg.Cookie))
r.Use(complianceportal.NewSessionHostMiddleware(cfg.Cookie))
r.Use(complianceportal.NewMemberProvisioningMiddleware(cfg.Visitor, cfg.Logger))
r.Handle(complianceportal.GraphQLPath, graphqlHandler)
},
)
r.Handle("/*", webServer)
r.NotFound(handleCustomDomain404)
},
)
return r, nil
}
func handleCustomDomain404(w http.ResponseWriter, r *http.Request) {
httpserver.RenderError(w, http.StatusNotFound, errors.New("not found"))
}
func compliancePageHeadData(baseURL *baseurl.BaseURL) HeadDataFunc {
return func(r *http.Request) HeadData {
tc := complianceportal.CompliancePageFromContext(r.Context())
if tc == nil {
return HeadData{Title: "Compliance Page"}
}
compliancePageBaseURL := complianceportal.CompliancePageBaseURLFromContext(r.Context())
description := tc.Title + " Compliance Page"
if tc.Description != nil && *tc.Description != "" {
description = *tc.Description
}
headData := HeadData{
Title: tc.Title,
Description: description,
OGURL: ref.UnrefOrZero(compliancePageBaseURL),
}
if tc.LogoFileID != nil {
faviconURL, err := baseURL.WithPath("/api/files/v1/public/" + tc.LogoFileID.String()).String()
if err == nil {
headData.FaviconURL = faviconURL
}
}
return headData
}
}

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package trust_v1
package complianceportal_v1
import (
"context"

View File

@@ -1,4 +1,4 @@
package trust_v1
package complianceportal_v1
// This file will be automatically regenerated based on the schema, any resolver
// implementations
@@ -15,16 +15,17 @@ import (
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/complianceportal"
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/schema"
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
)
// AcceptElectronicSignature is the resolver for the acceptElectronicSignature field.
func (r *mutationResolver) AcceptElectronicSignature(ctx context.Context, input types.AcceptElectronicSignatureInput) (*types.AcceptElectronicSignaturePayload, error) {
var (
identity = authn.IdentityFromContext(ctx)
httpReq = gqlutils.HTTPRequestFromContext(ctx)
identity = authn.IdentityFromContext(ctx)
httpReq = gqlutils.HTTPRequestFromContext(ctx)
trustCenter = complianceportal.CompliancePageFromContext(ctx)
)
signerIP, _, _ := net.SplitHostPort(httpReq.RemoteAddr)
@@ -32,8 +33,11 @@ func (r *mutationResolver) AcceptElectronicSignature(ctx context.Context, input
signerIP = httpReq.RemoteAddr
}
scope := coredata.NewScopeFromObjectID(trustCenter.ID)
signature, err := r.esign.AcceptSignature(
ctx,
scope,
&esign.AcceptSignatureRequest{
SignatureID: input.SignatureID,
SignerFullName: identity.FullName,
@@ -55,8 +59,9 @@ func (r *mutationResolver) AcceptElectronicSignature(ctx context.Context, input
// RecordSigningEvent is the resolver for the recordSigningEvent field.
func (r *mutationResolver) RecordSigningEvent(ctx context.Context, input types.RecordSigningEventInput) (*types.RecordSigningEventPayload, error) {
var (
identity = authn.IdentityFromContext(ctx)
httpReq = gqlutils.HTTPRequestFromContext(ctx)
identity = authn.IdentityFromContext(ctx)
httpReq = gqlutils.HTTPRequestFromContext(ctx)
trustCenter = complianceportal.CompliancePageFromContext(ctx)
)
actorIP, _, _ := net.SplitHostPort(httpReq.RemoteAddr)
@@ -64,8 +69,11 @@ func (r *mutationResolver) RecordSigningEvent(ctx context.Context, input types.R
actorIP = httpReq.RemoteAddr
}
scope := coredata.NewScopeFromObjectID(trustCenter.ID)
if err := r.esign.RecordEvent(
ctx,
scope,
&esign.RecordEventRequest{
SignatureID: input.SignatureID,
EventType: input.EventType,
@@ -132,7 +140,7 @@ func (r *nonDisclosureAgreementResolver) ViewerSignature(ctx context.Context, ob
return nil, nil
}
sig, err := r.esign.GetSignatureByID(ctx, *access.ElectronicSignatureID)
sig, err := r.esign.GetSignatureByID(ctx, scope, *access.ElectronicSignatureID)
if err != nil {
return nil, nil
}

View File

@@ -0,0 +1,173 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal_v1
import (
"errors"
"net/http"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/complianceportal/visitor"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/iam/oauth2"
"go.probo.inc/probo/pkg/saferedirect"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/complianceportal"
)
var (
errNotFound = errors.New("not found")
errInvalidContinueURL = errors.New("invalid continue URL")
errInternal = errors.New("internal server error")
errInvalidOAuthRequest = errors.New("invalid oauth request")
)
type OAuthCallbackHandler struct {
iam *iam.Service
visitor *visitor.Service
sessionCookie *authn.Cookie
safeRedirect *saferedirect.SafeRedirect
logger *log.Logger
}
func NewOAuthCallbackHandler(
iamSvc *iam.Service,
visitorSvc *visitor.Service,
cookieConfig securecookie.Config,
allowedHost saferedirect.AllowedHostFunc,
logger *log.Logger,
) *OAuthCallbackHandler {
return &OAuthCallbackHandler{
iam: iamSvc,
visitor: visitorSvc,
sessionCookie: authn.NewCookie(&cookieConfig),
safeRedirect: saferedirect.New(allowedHost),
logger: logger,
}
}
func (h *OAuthCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if oauthErr := r.URL.Query().Get("error"); oauthErr != "" {
h.logger.WarnCtx(
ctx,
"oauth callback returned error",
log.String("error", oauthErr),
log.String("error_description", r.URL.Query().Get("error_description")),
)
httpserver.RenderError(w, http.StatusBadRequest, errInvalidOAuthRequest)
return
}
code := r.URL.Query().Get("code")
stateToken := r.URL.Query().Get("state")
if code == "" || stateToken == "" {
httpserver.RenderError(w, http.StatusBadRequest, errInvalidOAuthRequest)
return
}
state, err := h.visitor.ConsumeOAuthState(ctx, stateToken)
if err != nil {
h.logger.WarnCtx(ctx, "invalid oauth state", log.Error(err))
httpserver.RenderError(w, http.StatusBadRequest, errInvalidOAuthRequest)
return
}
portal := complianceportal.CompliancePageFromContext(ctx)
portalBaseURL := complianceportal.CompliancePageBaseURLFromContext(ctx)
if portal == nil || portalBaseURL == nil {
httpserver.RenderError(w, http.StatusNotFound, errNotFound)
return
}
clientID, err := complianceportal.CIMDClientIDURL(*portalBaseURL)
if err != nil {
h.logger.ErrorCtx(ctx, "cannot build cimd client_id", log.Error(err))
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
return
}
redirectURI, err := complianceportal.OAuthCallbackURL(*portalBaseURL)
if err != nil {
h.logger.ErrorCtx(ctx, "cannot build oauth redirect_uri", log.Error(err))
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
return
}
tokenResult, err := h.iam.OAuth2ServerService.ExchangeAuthorizationCode(
ctx,
clientID,
code,
redirectURI,
state.CodeVerifier,
)
if err != nil {
h.logger.WarnCtx(ctx, "cannot exchange authorization code", log.Error(err))
httpserver.RenderError(w, http.StatusBadRequest, errInvalidOAuthRequest)
return
}
identityID, err := oauth2.ParseIDTokenIdentity(tokenResult.IDToken, state.Nonce)
if err != nil {
h.logger.WarnCtx(ctx, "cannot validate id token", log.Error(err))
httpserver.RenderError(w, http.StatusBadRequest, errInvalidOAuthRequest)
return
}
host, ok := complianceportal.TrustedRequestHost(r)
if !ok {
httpserver.RenderError(w, http.StatusBadRequest, errInvalidOAuthRequest)
return
}
session, err := h.iam.AuthService.OpenRootSession(
ctx,
identityID,
coredata.AuthMethodOIDC,
coredata.SessionDataForHost(host),
)
if err != nil {
h.logger.ErrorCtx(ctx, "cannot open session", log.Error(err))
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
return
}
if _, err := h.visitor.ProvisionPortalMember(ctx, portal.ID, identityID); err != nil {
h.logger.ErrorCtx(ctx, "cannot provision portal member", log.Error(err))
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
return
}
h.sessionCookie.Set(w, session)
continueURL := state.ContinueURL
if continueURL == "" {
continueURL = "/"
}
h.safeRedirect.Redirect(w, r, continueURL, "/", http.StatusFound)
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal_v1
import (
"encoding/json"
"net/http"
"go.gearno.de/kit/httpserver"
portal "go.probo.inc/probo/pkg/complianceportal"
"go.probo.inc/probo/pkg/server/api/complianceportal"
)
type oauthClientMetadataHandler struct{}
func NewOAuthClientMetadataHandler() http.Handler {
return &oauthClientMetadataHandler{}
}
func (h *oauthClientMetadataHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
compliancePage := complianceportal.CompliancePageFromContext(r.Context())
baseURL := complianceportal.CompliancePageBaseURLFromContext(r.Context())
if compliancePage == nil || baseURL == nil {
httpserver.RenderError(w, http.StatusNotFound, errNotFound)
return
}
doc, err := portal.BuildClientMetadataDocument(compliancePage, *baseURL)
if err != nil {
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "public, max-age=300")
_ = json.NewEncoder(w).Encode(doc)
}

View File

@@ -0,0 +1,116 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal_v1
import (
"net/http"
"go.gearno.de/kit/httpclient"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/complianceportal/visitor"
"go.probo.inc/probo/pkg/iam/oauth2"
"go.probo.inc/probo/pkg/saferedirect"
"go.probo.inc/probo/pkg/server/api/complianceportal"
)
type OAuthInitiateHandler struct {
proboBaseURL *baseurl.BaseURL
visitor *visitor.Service
safeRedirect *saferedirect.SafeRedirect
httpClient *http.Client
logger *log.Logger
}
func NewOAuthInitiateHandler(
proboBaseURL *baseurl.BaseURL,
visitorSvc *visitor.Service,
allowedHost saferedirect.AllowedHostFunc,
logger *log.Logger,
) *OAuthInitiateHandler {
return &OAuthInitiateHandler{
proboBaseURL: proboBaseURL,
visitor: visitorSvc,
safeRedirect: saferedirect.New(allowedHost),
httpClient: httpclient.DefaultClient(
httpclient.WithLogger(logger),
httpclient.WithSSRFProtection(),
),
logger: logger,
}
}
func (h *OAuthInitiateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
portalBaseURL := complianceportal.CompliancePageBaseURLFromContext(ctx)
if portalBaseURL == nil {
httpserver.RenderError(w, http.StatusNotFound, errNotFound)
return
}
continueURL := r.URL.Query().Get("continue")
if continueURL == "" {
continueURL = "/overview"
}
safeContinue, ok := h.safeRedirect.Validate(ctx, continueURL)
if !ok {
httpserver.RenderError(w, http.StatusBadRequest, errInvalidContinueURL)
return
}
clientID, err := complianceportal.CIMDClientIDURL(*portalBaseURL)
if err != nil {
h.logger.ErrorCtx(ctx, "cannot build cimd client_id", log.Error(err))
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
return
}
redirectURI, err := complianceportal.OAuthCallbackURL(*portalBaseURL)
if err != nil {
h.logger.ErrorCtx(ctx, "cannot build oauth redirect_uri", log.Error(err))
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
return
}
metadata, err := oauth2.FetchServerMetadata(ctx, h.httpClient, h.proboBaseURL.String())
if err != nil {
h.logger.ErrorCtx(ctx, "cannot fetch discovery metadata", log.Error(err))
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
return
}
authorizeURL, err := h.visitor.InitiateOAuthAuthorizeURL(
ctx,
metadata.AuthorizationEndpoint.String(),
clientID,
redirectURI,
[]string{complianceportal.VisitorOAuthScope},
safeContinue,
)
if err != nil {
h.logger.ErrorCtx(ctx, "cannot initiate oauth authorize", log.Error(err))
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
return
}
http.Redirect(w, r, authorizeURL, http.StatusFound)
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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.
//go:generate go run github.com/99designs/gqlgen generate
package complianceportal_v1
import (
"time"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
trust "go.probo.inc/probo/pkg/complianceportal/visitor"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/filemanager"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/resourcealias"
"go.probo.inc/probo/pkg/server/api/authn"
)
type (
TrustAuthConfig struct {
CookieName string
CookieDomain string
CookieDuration time.Duration
TokenDuration time.Duration
ReportURLDuration time.Duration
Scope string
TokenType string
CookieSecure bool
}
Resolver struct {
trust *trust.Service
resourceAlias *resourcealias.Service
fileManager *filemanager.Service
esign *esign.Service
mailman *mailman.Service
logger *log.Logger
iam *iam.Service
sessionCookie *authn.Cookie
baseURL *baseurl.BaseURL
}
)

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package trust_v1
package complianceportal_v1
import (
"context"

View File

@@ -1,4 +1,4 @@
package trust_v1
package complianceportal_v1
// This file will be automatically regenerated based on the schema, any resolver
// implementations
@@ -9,12 +9,12 @@ import (
"context"
"go.gearno.de/kit/log"
trust "go.probo.inc/probo/pkg/complianceportal/visitor"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/compliancepage"
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
"go.probo.inc/probo/pkg/server/api/complianceportal"
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/trust"
)
// CreateRightsRequest is the resolver for the createRightsRequest field.
@@ -26,10 +26,10 @@ func (r *mutationResolver) CreateRightsRequest(ctx context.Context, input types.
return nil, gqlutils.Unauthenticatedf(ctx, "a verified email is required to submit a request")
}
compliancePage := compliancepage.CompliancePageFromContext(ctx)
compliancePage := complianceportal.CompliancePageFromContext(ctx)
scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID)
rightsRequest, err := r.trust.RightsRequests.Create(
rightsRequest, err := r.trust.CreateRightsRequest(
ctx,
scope,
&trust.CreateRightsRequest{

View File

@@ -18,8 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// Package trust provides functionality for serving the trust center SPA frontend.
package trust
package complianceportal_v1
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package trust_v1
package complianceportal_v1
// This file will be automatically regenerated based on the schema, any resolver
// implementations
@@ -18,8 +18,8 @@ import (
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/complianceportal"
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/schema"
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
)
@@ -165,7 +165,13 @@ func (r *complianceFrameworkResolver) Framework(ctx context.Context, obj *types.
scope := coredata.NewScopeFromObjectID(obj.ID)
trustService := r.trust
framework, err := trustService.GetFramework(ctx, scope, obj.FrameworkID)
complianceFramework, err := trustService.GetComplianceFramework(ctx, scope, obj.ID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot load compliance framework", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
framework, err := trustService.GetFramework(ctx, scope, complianceFramework.FrameworkID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot load framework", log.Error(err))
return nil, gqlutils.Internal(ctx)
@@ -827,9 +833,10 @@ func (r *trustCenterResolver) Subprocessors(ctx context.Context, obj *types.Trus
// SubprocessorCategories is the resolver for the subprocessorCategories field.
func (r *trustCenterResolver) SubprocessorCategories(ctx context.Context, obj *types.TrustCenter) ([]coredata.ThirdPartyCategory, error) {
trustCenter := complianceportal.CompliancePageFromContext(ctx)
scope := coredata.NewScopeFromObjectID(obj.ID)
categories, err := r.trust.ListDistinctTrustCenterCategoriesForOrganizationID(ctx, scope, obj.Organization.ID)
categories, err := r.trust.ListDistinctTrustCenterCategoriesForOrganizationID(ctx, scope, trustCenter.OrganizationID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list subprocessor categories", log.Error(err))
return nil, gqlutils.Internal(ctx)
@@ -840,9 +847,10 @@ func (r *trustCenterResolver) SubprocessorCategories(ctx context.Context, obj *t
// SubprocessorCountries is the resolver for the subprocessorCountries field.
func (r *trustCenterResolver) SubprocessorCountries(ctx context.Context, obj *types.TrustCenter) ([]coredata.CountryCode, error) {
trustCenter := complianceportal.CompliancePageFromContext(ctx)
scope := coredata.NewScopeFromObjectID(obj.ID)
countries, err := r.trust.ListDistinctTrustCenterCountriesForOrganizationID(ctx, scope, obj.Organization.ID)
countries, err := r.trust.ListDistinctTrustCenterCountriesForOrganizationID(ctx, scope, trustCenter.OrganizationID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list subprocessor countries", log.Error(err))
return nil, gqlutils.Internal(ctx)

View File

@@ -22,33 +22,17 @@ package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type ComplianceFramework struct {
ID gid.GID `json:"id"`
Framework *Framework `json:"framework"`
FrameworkID gid.GID `json:"-"`
}
func (ComplianceFramework) IsNode() {}
func (cf ComplianceFramework) GetID() gid.GID { return cf.ID }
type ComplianceFrameworkConnection struct {
Edges []*ComplianceFrameworkEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
}
type ComplianceFrameworkEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *ComplianceFramework `json:"node"`
}
func NewComplianceFramework(cf *coredata.ComplianceFramework) *ComplianceFramework {
return &ComplianceFramework{
ID: cf.ID,
FrameworkID: cf.FrameworkID,
ID: cf.ID,
}
}

View File

@@ -21,25 +21,9 @@
package types
import (
"time"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
)
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 (m MailingListSubscriber) GetID() gid.GID { return m.ID }
func NewMailingListSubscriber(s *coredata.MailingListSubscriber) *MailingListSubscriber {
return &MailingListSubscriber{
ID: s.ID,

View File

@@ -1,141 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//go:generate go tool github.com/99designs/gqlgen generate
// Copyright (c) 2025 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package trust_v1
import (
"context"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
trust "go.probo.inc/probo/pkg/complianceportal/visitor"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/filemanager"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/resourcealias"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/complianceportal"
"go.probo.inc/probo/pkg/server/gqlutils"
)
type (
TrustAuthConfig struct {
CookieName string
CookieDomain string
CookieDuration time.Duration
TokenDuration time.Duration
ReportURLDuration time.Duration
Scope string
TokenType string
CookieSecure bool
}
Resolver struct {
trust *trust.Service
resourceAlias *resourcealias.Service
fileManager *filemanager.Service
esign *esign.Service
mailman *mailman.Service
logger *log.Logger
iam *iam.Service
sessionCookie *authn.Cookie
baseURL *baseurl.BaseURL
}
)
func NewMux(
logger *log.Logger,
iamSvc *iam.Service,
trustSvc *trust.Service,
resourceAliasSvc *resourcealias.Service,
fileManagerSvc *filemanager.Service,
esignSvc *esign.Service,
mailmanSvc *mailman.Service,
cookieConfig securecookie.Config,
tokenSecret string,
baseURL *baseurl.BaseURL,
graphqlLimits gqlutils.Limits,
) *chi.Mux {
r := chi.NewMux()
r.Use(complianceportal.NewCompliancePagePresenceMiddleware())
sessionTransferHandler := NewSessionTransferHandler(
iamSvc,
cookieConfig,
func(ctx context.Context, host string) bool {
_, err := trustSvc.GetPortalByDomainName(ctx, host)
return err == nil
},
logger,
)
r.Method(http.MethodGet, "/session-transfer", sessionTransferHandler)
graphqlHandler := NewGraphQLHandler(
iamSvc,
trustSvc,
resourceAliasSvc,
fileManagerSvc,
esignSvc,
mailmanSvc,
logger,
baseURL,
cookieConfig,
tokenSecret,
graphqlLimits,
)
r.Group(
func(r chi.Router) {
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
r.Use(complianceportal.NewMemberProvisioningMiddleware(trustSvc, logger))
r.Handle("/graphql", graphqlHandler)
},
)
return r
}