From 65b7d90d615369451206ace5d0eb1bcfe10cd749 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Wed, 8 Oct 2025 20:33:31 +0200 Subject: [PATCH] Update trust center handler Signed-off-by: Bryan Frimin --- pkg/probo/service.go | 31 +++- pkg/probod/probod.go | 2 +- pkg/server/api/api.go | 78 ++++----- pkg/server/api/trust/v1/schema.graphql | 17 +- pkg/server/api/trust/v1/schema/schema.go | 143 ++++++++++++++-- .../trust/v1/trust_center_access_handler.go | 16 ++ pkg/server/api/trust/v1/v1_resolver.go | 39 +++++ pkg/server/server.go | 152 +++++++++++++++--- pkg/trust/trust_center_service.go | 25 +++ 9 files changed, 418 insertions(+), 85 deletions(-) diff --git a/pkg/probo/service.go b/pkg/probo/service.go index fd16d4d93..b9561c016 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -98,11 +98,8 @@ type ( Snapshots *SnapshotService ContinualImprovements *ContinualImprovementService ProcessingActivities *ProcessingActivityService -<<<<<<< HEAD Files *FileService -======= CustomDomains *CustomDomainService ->>>>>>> 087e86b5 (Add service impl) } ) @@ -213,7 +210,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService { acmeService: s.acmeService, logger: s.logger.Named("custom_domains"), } - + return tenantService } @@ -359,3 +356,29 @@ func (s *Service) LoadOrganizationByDomain(ctx context.Context, domain string) ( return organizationID, err } + +type TrustCenterInfo struct { + ID gid.GID + OrganizationID gid.GID +} + +func (s *Service) LoadTrustCenterBySlug(ctx context.Context, slug string) (*TrustCenterInfo, error) { + var info TrustCenterInfo + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + var trustCenter coredata.TrustCenter + if err := trustCenter.LoadBySlug(ctx, conn, slug); err != nil { + return fmt.Errorf("cannot load trust center: %w", err) + } + + info.ID = trustCenter.ID + info.OrganizationID = trustCenter.OrganizationID + + return nil + }, + ) + + return &info, err +} diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 3f3d9cf56..db1ec5e9e 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -392,7 +392,7 @@ func (impl *Implm) Run( defer stopTrustCenterServer() wg.Go( func() { - if err := impl.runTrustCenterServer(trustCenterServerCtx, l, r, tp, pgClient, serverHandler, acmeService); err != nil { + if err := impl.runTrustCenterServer(trustCenterServerCtx, l, r, tp, pgClient, serverHandler.TrustCenterHandler(), acmeService); err != nil { cancel(fmt.Errorf("trust center server crashed: %w", err)) } }, diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go index 380c44b98..24e8e2326 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -53,20 +53,21 @@ type ( } Config struct { - AllowedOrigins []string - Probo *probo.Service - Usrmgr *usrmgr.Service - Trust *trust.Service - Auth ConsoleAuthConfig - TrustAuth TrustAuthConfig - ConnectorRegistry *connector.ConnectorRegistry - SafeRedirect *saferedirect.SafeRedirect - CustomDomainCname string - Logger *log.Logger + AllowedOrigins []string + Probo *probo.Service + Usrmgr *usrmgr.Service + Trust *trust.Service + Auth ConsoleAuthConfig + TrustAuth TrustAuthConfig + ConnectorRegistry *connector.ConnectorRegistry + SafeRedirect *saferedirect.SafeRedirect + CustomDomainCname string + Logger *log.Logger } Server struct { - cfg Config + cfg Config + trustAPIHandler http.Handler } ) @@ -108,11 +109,39 @@ func NewServer(cfg Config) (*Server, error) { return nil, ErrMissingUsrmgrService } + // Create trust API handler once + trustAPIHandler := trust_v1.NewMux( + cfg.Logger.Named("trust.v1"), + cfg.Usrmgr, + cfg.Trust, + console_v1.AuthConfig{ + CookieName: cfg.Auth.CookieName, + CookieDomain: cfg.Auth.CookieDomain, + SessionDuration: cfg.Auth.SessionDuration, + CookieSecret: cfg.Auth.CookieSecret, + }, + trust_v1.TrustAuthConfig{ + CookieName: cfg.TrustAuth.CookieName, + CookieDomain: cfg.TrustAuth.CookieDomain, + CookieDuration: cfg.TrustAuth.CookieDuration, + TokenDuration: cfg.TrustAuth.TokenDuration, + ReportURLDuration: cfg.TrustAuth.ReportURLDuration, + TokenSecret: cfg.TrustAuth.TokenSecret, + Scope: cfg.TrustAuth.Scope, + TokenType: cfg.TrustAuth.TokenType, + }, + ) + return &Server{ - cfg: cfg, + cfg: cfg, + trustAPIHandler: trustAPIHandler, }, nil } +func (s *Server) TrustAPIHandler() http.Handler { + return s.trustAPIHandler +} + func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { corsOpts := cors.Options{ AllowedOrigins: s.cfg.AllowedOrigins, @@ -160,30 +189,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { ) // Mount the trust API with authentication - router.Mount( - "/trust/v1", - trust_v1.NewMux( - s.cfg.Logger.Named("trust.v1"), - s.cfg.Usrmgr, - s.cfg.Trust, - console_v1.AuthConfig{ - CookieName: s.cfg.Auth.CookieName, - CookieDomain: s.cfg.Auth.CookieDomain, - SessionDuration: s.cfg.Auth.SessionDuration, - CookieSecret: s.cfg.Auth.CookieSecret, - }, - trust_v1.TrustAuthConfig{ - CookieName: s.cfg.TrustAuth.CookieName, - CookieDomain: s.cfg.TrustAuth.CookieDomain, - CookieDuration: s.cfg.TrustAuth.CookieDuration, - TokenDuration: s.cfg.TrustAuth.TokenDuration, - ReportURLDuration: s.cfg.TrustAuth.ReportURLDuration, - TokenSecret: s.cfg.TrustAuth.TokenSecret, - Scope: s.cfg.TrustAuth.Scope, - TokenType: s.cfg.TrustAuth.TokenType, - }, - ), - ) + router.Mount("/trust/v1", s.trustAPIHandler) router.ServeHTTP(w, r) } diff --git a/pkg/server/api/trust/v1/schema.graphql b/pkg/server/api/trust/v1/schema.graphql index 5c8f51785..0f234f2c6 100644 --- a/pkg/server/api/trust/v1/schema.graphql +++ b/pkg/server/api/trust/v1/schema.graphql @@ -71,7 +71,6 @@ type DocumentEdge { node: Document! } - type Framework implements Node { id: ID! name: String! @@ -572,20 +571,18 @@ type AcceptNonDisclosureAgreementPayload { type Query { node(id: ID!): Node! trustCenterBySlug(slug: String!): TrustCenter @mustBeAuthenticated(role: NONE) + currentTrustCenter: TrustCenter @mustBeAuthenticated(role: NONE) } type Mutation { - requestAllAccesses( - input: RequestAllAccessesInput! - ): RequestAccessesPayload! @mustBeAuthenticated(role: NONE) + requestAllAccesses(input: RequestAllAccessesInput!): RequestAccessesPayload! + @mustBeAuthenticated(role: NONE) - exportDocumentPDF( - input: ExportDocumentPDFInput! - ): ExportDocumentPDFPayload! @mustBeAuthenticated(role: NONE) + exportDocumentPDF(input: ExportDocumentPDFInput!): ExportDocumentPDFPayload! + @mustBeAuthenticated(role: NONE) - exportReportPDF( - input: ExportReportPDFInput! - ): ExportReportPDFPayload! @mustBeAuthenticated(role: NONE) + exportReportPDF(input: ExportReportPDFInput!): ExportReportPDFPayload! + @mustBeAuthenticated(role: NONE) acceptNonDisclosureAgreement( input: AcceptNonDisclosureAgreementInput! diff --git a/pkg/server/api/trust/v1/schema/schema.go b/pkg/server/api/trust/v1/schema/schema.go index 829a47473..353b8c103 100644 --- a/pkg/server/api/trust/v1/schema/schema.go +++ b/pkg/server/api/trust/v1/schema/schema.go @@ -137,8 +137,9 @@ type ComplexityRoot struct { } Query struct { - Node func(childComplexity int, id gid.GID) int - TrustCenterBySlug func(childComplexity int, slug string) int + CurrentTrustCenter func(childComplexity int) int + Node func(childComplexity int, id gid.GID) int + TrustCenterBySlug func(childComplexity int, slug string) int } Report struct { @@ -235,6 +236,7 @@ type OrganizationResolver interface { type QueryResolver interface { Node(ctx context.Context, id gid.GID) (types.Node, error) TrustCenterBySlug(ctx context.Context, slug string) (*types.TrustCenter, error) + CurrentTrustCenter(ctx context.Context) (*types.TrustCenter, error) } type ReportResolver interface { IsUserAuthorized(ctx context.Context, obj *types.Report) (bool, error) @@ -569,6 +571,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.PageInfo.StartCursor(childComplexity), true + case "Query.currentTrustCenter": + if e.complexity.Query.CurrentTrustCenter == nil { + break + } + + return e.complexity.Query.CurrentTrustCenter(childComplexity), true + case "Query.node": if e.complexity.Query.Node == nil { break @@ -1084,7 +1093,6 @@ type DocumentEdge { node: Document! } - type Framework implements Node { id: ID! name: String! @@ -1585,20 +1593,18 @@ type AcceptNonDisclosureAgreementPayload { type Query { node(id: ID!): Node! trustCenterBySlug(slug: String!): TrustCenter @mustBeAuthenticated(role: NONE) + currentTrustCenter: TrustCenter @mustBeAuthenticated(role: NONE) } type Mutation { - requestAllAccesses( - input: RequestAllAccessesInput! - ): RequestAccessesPayload! @mustBeAuthenticated(role: NONE) + requestAllAccesses(input: RequestAllAccessesInput!): RequestAccessesPayload! + @mustBeAuthenticated(role: NONE) - exportDocumentPDF( - input: ExportDocumentPDFInput! - ): ExportDocumentPDFPayload! @mustBeAuthenticated(role: NONE) + exportDocumentPDF(input: ExportDocumentPDFInput!): ExportDocumentPDFPayload! + @mustBeAuthenticated(role: NONE) - exportReportPDF( - input: ExportReportPDFInput! - ): ExportReportPDFPayload! @mustBeAuthenticated(role: NONE) + exportReportPDF(input: ExportReportPDFInput!): ExportReportPDFPayload! + @mustBeAuthenticated(role: NONE) acceptNonDisclosureAgreement( input: AcceptNonDisclosureAgreementInput! @@ -4391,6 +4397,100 @@ func (ec *executionContext) fieldContext_Query_trustCenterBySlug(ctx context.Con return fc, nil } +func (ec *executionContext) _Query_currentTrustCenter(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_currentTrustCenter(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + directive0 := func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().CurrentTrustCenter(rctx) + } + + directive1 := func(ctx context.Context) (any, error) { + role, err := ec.unmarshalORole2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRole(ctx, "NONE") + if err != nil { + var zeroVal *types.TrustCenter + return zeroVal, err + } + if ec.directives.MustBeAuthenticated == nil { + var zeroVal *types.TrustCenter + return zeroVal, errors.New("directive mustBeAuthenticated is not implemented") + } + return ec.directives.MustBeAuthenticated(ctx, nil, directive0, role) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*types.TrustCenter); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/getprobo/probo/pkg/server/api/trust/v1/types.TrustCenter`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*types.TrustCenter) + fc.Result = res + return ec.marshalOTrustCenter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐTrustCenter(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_currentTrustCenter(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_TrustCenter_id(ctx, field) + case "active": + return ec.fieldContext_TrustCenter_active(ctx, field) + case "slug": + return ec.fieldContext_TrustCenter_slug(ctx, field) + case "ndaFileName": + return ec.fieldContext_TrustCenter_ndaFileName(ctx, field) + case "ndaFileUrl": + return ec.fieldContext_TrustCenter_ndaFileUrl(ctx, field) + case "organization": + return ec.fieldContext_TrustCenter_organization(ctx, field) + case "isUserAuthenticated": + return ec.fieldContext_TrustCenter_isUserAuthenticated(ctx, field) + case "hasAcceptedNonDisclosureAgreement": + return ec.fieldContext_TrustCenter_hasAcceptedNonDisclosureAgreement(ctx, field) + case "documents": + return ec.fieldContext_TrustCenter_documents(ctx, field) + case "audits": + return ec.fieldContext_TrustCenter_audits(ctx, field) + case "vendors": + return ec.fieldContext_TrustCenter_vendors(ctx, field) + case "references": + return ec.fieldContext_TrustCenter_references(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type TrustCenter", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query___type(ctx, field) if err != nil { @@ -9559,6 +9659,25 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "currentTrustCenter": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_currentTrustCenter(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "__type": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { diff --git a/pkg/server/api/trust/v1/trust_center_access_handler.go b/pkg/server/api/trust/v1/trust_center_access_handler.go index e006eaf85..813815307 100644 --- a/pkg/server/api/trust/v1/trust_center_access_handler.go +++ b/pkg/server/api/trust/v1/trust_center_access_handler.go @@ -21,6 +21,7 @@ import ( "net/http" "time" + "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/probo" console_v1 "github.com/getprobo/probo/pkg/server/api/console/v1" "github.com/getprobo/probo/pkg/server/session" @@ -29,6 +30,21 @@ import ( "go.gearno.de/kit/httpserver" ) +var ( + CustomDomainTenantIDKey = &ctxKey{name: "custom_domain_tenant_id"} + CustomDomainOrganizationIDKey = &ctxKey{name: "custom_domain_organization_id"} +) + +func GetCustomDomainTenantID(ctx context.Context) (gid.TenantID, bool) { + tenantID, ok := ctx.Value(CustomDomainTenantIDKey).(gid.TenantID) + return tenantID, ok +} + +func GetCustomDomainOrganizationID(ctx context.Context) (gid.GID, bool) { + organizationID, ok := ctx.Value(CustomDomainOrganizationIDKey).(gid.GID) + return organizationID, ok +} + type ( AuthTokenRequest struct { Token string `json:"token"` diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go index bf616ad37..7ac668eb0 100644 --- a/pkg/server/api/trust/v1/v1_resolver.go +++ b/pkg/server/api/trust/v1/v1_resolver.go @@ -547,6 +547,45 @@ func (r *queryResolver) TrustCenterBySlug(ctx context.Context, slug string) (*ty return response, nil } +// CurrentTrustCenter is the resolver for the currentTrustCenter field. +func (r *queryResolver) CurrentTrustCenter(ctx context.Context) (*types.TrustCenter, error) { + // Get organization and tenant from custom domain context + organizationID, ok := GetCustomDomainOrganizationID(ctx) + if !ok { + return nil, fmt.Errorf("organization not found for custom domain") + } + + tenantID, ok := GetCustomDomainTenantID(ctx) + if !ok { + return nil, fmt.Errorf("tenant not found for custom domain") + } + + publicTrustService := r.PublicTrustService(ctx, tenantID) + + trustCenter, err := publicTrustService.TrustCenters.GetByOrganizationID(ctx, organizationID) + if err != nil { + return nil, fmt.Errorf("cannot load trust center: %w", err) + } + + if !trustCenter.Active { + return nil, nil + } + + trustCenter, file, err := publicTrustService.TrustCenters.Get(ctx, trustCenter.ID) + if err != nil { + panic(fmt.Errorf("cannot get trust center: %w", err)) + } + + org, err := publicTrustService.Organizations.Get(ctx, organizationID) + if err != nil { + panic(fmt.Errorf("cannot get organization: %w", err)) + } + response := types.NewTrustCenter(trustCenter, file) + response.Organization = types.NewOrganization(org) + + return response, nil +} + // IsUserAuthorized is the resolver for the isUserAuthorized field. func (r *reportResolver) IsUserAuthorized(ctx context.Context, obj *types.Report) (bool, error) { publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) diff --git a/pkg/server/server.go b/pkg/server/server.go index 73c25fb17..f30ee8516 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -16,14 +16,15 @@ package server import ( + "context" "net/http" - "strings" "github.com/getprobo/probo/pkg/agents" "github.com/getprobo/probo/pkg/connector" "github.com/getprobo/probo/pkg/probo" "github.com/getprobo/probo/pkg/saferedirect" "github.com/getprobo/probo/pkg/server/api" + trust_v1 "github.com/getprobo/probo/pkg/server/api/trust/v1" "github.com/getprobo/probo/pkg/server/trust" "github.com/getprobo/probo/pkg/server/web" trust_pkg "github.com/getprobo/probo/pkg/trust" @@ -55,6 +56,8 @@ type Server struct { trustServer *trust.Server router *chi.Mux extraHeaderFields map[string]string + proboService *probo.Service + logger *log.Logger } // NewServer creates a new server instance @@ -98,6 +101,8 @@ func NewServer(cfg Config) (*Server, error) { trustServer: trustServer, router: router, extraHeaderFields: cfg.ExtraHeaderFields, + proboService: cfg.Probo, + logger: cfg.Logger, } // Set up routes @@ -108,36 +113,139 @@ func NewServer(cfg Config) (*Server, error) { // setupRoutes configures the routing for the server func (s *Server) setupRoutes() { - // API routes under /api - s.router.Mount("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Strip the /api prefix from the path - r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api") - if r.URL.Path == "" { - r.URL.Path = "/" - } - s.apiServer.ServeHTTP(w, r) - })) + // API routes + s.router.Mount("/api", s.apiServer) - // Trust routes go to the trust SPA - s.router.Route("/trust", func(r chi.Router) { - r.Mount("/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - req.URL.Path = strings.TrimPrefix(req.URL.Path, "/trust") - if req.URL.Path == "" { - req.URL.Path = "/" - } - s.trustServer.ServeHTTP(w, req) - })) + // Trust center routes by slug + s.router.Route("/trust/{slug}", func(r chi.Router) { + r.Use(s.loadTrustCenterBySlug) + r.Mount("/", s.trustCenterRouter()) }) - // All other routes go to the console SPA frontend + // Console SPA (catch-all) s.router.Mount("/", s.webServer) } // ServeHTTP implements the http.Handler interface func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.setExtraHeaders(w) + s.router.ServeHTTP(w, r) +} + +// setExtraHeaders adds configured extra headers to the response +func (s *Server) setExtraHeaders(w http.ResponseWriter) { for key, value := range s.extraHeaderFields { w.Header().Set(key, value) } - - s.router.ServeHTTP(w, r) +} + +// loadTrustCenterBySlug middleware loads trust center info from slug and adds to context +func (s *Server) loadTrustCenterBySlug(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + slug := chi.URLParam(r, "slug") + + s.logger.InfoCtx(ctx, "loading trust center by slug", + log.String("slug", slug), + log.String("path", r.URL.Path), + ) + + trustCenter, err := s.proboService.LoadTrustCenterBySlug(ctx, slug) + if err != nil { + s.logger.WarnCtx(ctx, "trust center not found", + log.String("slug", slug), + log.Error(err), + ) + http.Error(w, "Trust center not found", http.StatusNotFound) + return + } + + s.logger.InfoCtx(ctx, "trust center loaded", + log.String("slug", slug), + log.String("trust_center_id", trustCenter.ID.String()), + log.String("organization_id", trustCenter.OrganizationID.String()), + ) + + ctx = s.addTrustCenterToContext(ctx, trustCenter.ID.TenantID(), trustCenter.OrganizationID) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// loadTrustCenterByDomain middleware loads trust center info from custom domain and adds to context +func (s *Server) loadTrustCenterByDomain(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if r.TLS == nil || r.TLS.ServerName == "" { + next.ServeHTTP(w, r) + return + } + + domain := r.TLS.ServerName + + s.logger.InfoCtx(ctx, "loading organization by custom domain", + log.String("domain", domain), + log.String("path", r.URL.Path), + ) + + organizationID, err := s.proboService.LoadOrganizationByDomain(ctx, domain) + if err != nil { + s.logger.WarnCtx(ctx, "organization not found for domain", + log.String("domain", domain), + log.Error(err), + ) + next.ServeHTTP(w, r) + return + } + + s.logger.InfoCtx(ctx, "organization loaded", + log.String("domain", domain), + log.String("organization_id", organizationID.String()), + ) + + ctx = s.addTrustCenterToContext(ctx, organizationID.TenantID(), organizationID) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// addTrustCenterToContext adds trust center identification to context +func (s *Server) addTrustCenterToContext(ctx context.Context, tenantID, organizationID interface{}) context.Context { + ctx = context.WithValue(ctx, trust_v1.CustomDomainTenantIDKey, tenantID) + ctx = context.WithValue(ctx, trust_v1.CustomDomainOrganizationIDKey, organizationID) + return ctx +} + +// trustCenterRouter returns a router for trust center content (API + frontend) +func (s *Server) trustCenterRouter() chi.Router { + r := chi.NewRouter() + + // Trust API routes + r.Mount("/api/trust/v1", s.apiServer.TrustAPIHandler()) + + // Trust center frontend (catch-all) + r.Handle("/*", s.trustServer) + + return r +} + +// TrustCenterHandler returns an HTTP handler for serving trust centers on custom domains +func (s *Server) TrustCenterHandler() http.Handler { + r := chi.NewRouter() + + // Set security headers + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Strict-Transport-Security", "max-age=31536000; preload") + s.setExtraHeaders(w) + next.ServeHTTP(w, r) + }) + }) + + // Load organization by custom domain + r.Use(s.loadTrustCenterByDomain) + + // Mount trust center content + r.Mount("/", s.trustCenterRouter()) + + return r } diff --git a/pkg/trust/trust_center_service.go b/pkg/trust/trust_center_service.go index 9a82eafe3..0b50a0ff9 100644 --- a/pkg/trust/trust_center_service.go +++ b/pkg/trust/trust_center_service.go @@ -89,6 +89,31 @@ func (s TrustCenterService) Get( return trustCenter, file, nil } +func (s TrustCenterService) GetByOrganizationID( + ctx context.Context, + organizationID gid.GID, +) (*coredata.TrustCenter, error) { + trustCenter := &coredata.TrustCenter{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + err := trustCenter.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID) + if err != nil { + return fmt.Errorf("cannot load trust center: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return trustCenter, nil +} + func (s TrustCenterService) GenerateNDAFileURL( ctx context.Context, trustCenterID gid.GID,