Resolve custom domain SSL via certificate
Replace flattened SSL fields with a certificate relation loaded through certmanager, and resolve domain slots from IDs already on the trust center instead of reloading the compliance page. Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
@@ -37,8 +37,10 @@ const fragment = graphql`
|
|||||||
id
|
id
|
||||||
domain
|
domain
|
||||||
managed
|
managed
|
||||||
sslStatus
|
certificate {
|
||||||
|
status
|
||||||
provisioningError
|
provisioningError
|
||||||
|
}
|
||||||
canDelete: permission(action: "compliance-portal:custom-domain:delete")
|
canDelete: permission(action: "compliance-portal:custom-domain:delete")
|
||||||
...CompliancePageDomainDialogFragment
|
...CompliancePageDomainDialogFragment
|
||||||
}
|
}
|
||||||
@@ -52,6 +54,8 @@ export function CompliancePageDomainCard(props: {
|
|||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
const domain = useFragment<CompliancePageDomainCardFragment$key>(fragment, fKey);
|
const domain = useFragment<CompliancePageDomainCardFragment$key>(fragment, fKey);
|
||||||
|
const sslStatus = domain.certificate?.status ?? "PENDING";
|
||||||
|
const provisioningError = domain.certificate?.provisioningError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padded>
|
<Card padded>
|
||||||
@@ -62,15 +66,15 @@ export function CompliancePageDomainCard(props: {
|
|||||||
{domain.managed && (
|
{domain.managed && (
|
||||||
<Badge variant="neutral">{__("Managed")}</Badge>
|
<Badge variant="neutral">{__("Managed")}</Badge>
|
||||||
)}
|
)}
|
||||||
<Badge variant={getCustomDomainStatusBadgeVariant(domain.sslStatus)}>
|
<Badge variant={getCustomDomainStatusBadgeVariant(sslStatus)}>
|
||||||
{getCustomDomainStatusBadgeLabel(domain.sslStatus, __)}
|
{getCustomDomainStatusBadgeLabel(sslStatus, __)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-txt-secondary">
|
<p className="text-sm text-txt-secondary">
|
||||||
{domain.sslStatus === "ACTIVE"
|
{sslStatus === "ACTIVE"
|
||||||
? __("Verified and serving traffic")
|
? __("Verified and serving traffic")
|
||||||
: domain.provisioningError
|
: provisioningError
|
||||||
? domain.provisioningError
|
? provisioningError
|
||||||
: __("Pending DNS verification")}
|
: __("Pending DNS verification")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,9 +39,12 @@ import type { CompliancePageDomainDialogFragment$key } from "#/__generated__/cor
|
|||||||
|
|
||||||
const fragment = graphql`
|
const fragment = graphql`
|
||||||
fragment CompliancePageDomainDialogFragment on CustomDomain {
|
fragment CompliancePageDomainDialogFragment on CustomDomain {
|
||||||
sslStatus
|
|
||||||
domain
|
domain
|
||||||
|
certificate {
|
||||||
|
status
|
||||||
|
expiresAt
|
||||||
provisioningError
|
provisioningError
|
||||||
|
}
|
||||||
dnsRecords {
|
dnsRecords {
|
||||||
type
|
type
|
||||||
name
|
name
|
||||||
@@ -49,7 +52,6 @@ const fragment = graphql`
|
|||||||
ttl
|
ttl
|
||||||
purpose
|
purpose
|
||||||
}
|
}
|
||||||
sslExpiresAt
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -72,6 +74,9 @@ export function CompliancePageDomainDialog(props: CompliancePageDomainDialogProp
|
|||||||
};
|
};
|
||||||
|
|
||||||
const domain = useFragment<CompliancePageDomainDialogFragment$key>(fragment, fKey);
|
const domain = useFragment<CompliancePageDomainDialogFragment$key>(fragment, fKey);
|
||||||
|
const sslStatus = domain.certificate?.status ?? "PENDING";
|
||||||
|
const expiresAt = domain.certificate?.expiresAt;
|
||||||
|
const provisioningError = domain.certificate?.provisioningError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -80,14 +85,14 @@ export function CompliancePageDomainDialog(props: CompliancePageDomainDialogProp
|
|||||||
title={(
|
title={(
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span>{domain.domain}</span>
|
<span>{domain.domain}</span>
|
||||||
<Badge variant={getCustomDomainStatusBadgeVariant(domain.sslStatus)}>
|
<Badge variant={getCustomDomainStatusBadgeVariant(sslStatus)}>
|
||||||
{getCustomDomainStatusBadgeLabel(domain.sslStatus, __)}
|
{getCustomDomainStatusBadgeLabel(sslStatus, __)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DialogContent padded className="space-y-6">
|
<DialogContent padded className="space-y-6">
|
||||||
{domain.sslStatus === "ACTIVE"
|
{sslStatus === "ACTIVE"
|
||||||
? (
|
? (
|
||||||
<div className="bg-subtle rounded-lg p-4">
|
<div className="bg-subtle rounded-lg p-4">
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
@@ -109,11 +114,11 @@ export function CompliancePageDomainDialog(props: CompliancePageDomainDialogProp
|
|||||||
"Your custom domain is verified and SSL certificate is active",
|
"Your custom domain is verified and SSL certificate is active",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{domain.sslExpiresAt && (
|
{expiresAt && (
|
||||||
<p className="text-xs text-txt-tertiary mt-2">
|
<p className="text-xs text-txt-tertiary mt-2">
|
||||||
{__("SSL expires")}
|
{__("SSL expires")}
|
||||||
{" "}
|
{" "}
|
||||||
{new Date(domain.sslExpiresAt).toLocaleDateString()}
|
{new Date(expiresAt).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -122,10 +127,10 @@ export function CompliancePageDomainDialog(props: CompliancePageDomainDialogProp
|
|||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<div>
|
<div>
|
||||||
{domain.provisioningError && (
|
{provisioningError && (
|
||||||
<div className="bg-danger-subtle text-danger rounded-lg p-4 mb-4">
|
<div className="bg-danger-subtle text-danger rounded-lg p-4 mb-4">
|
||||||
<p className="text-sm font-medium mb-1">{__("Provisioning error")}</p>
|
<p className="text-sm font-medium mb-1">{__("Provisioning error")}</p>
|
||||||
<p className="text-sm">{domain.provisioningError}</p>
|
<p className="text-sm">{provisioningError}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -188,7 +193,7 @@ export function CompliancePageDomainDialog(props: CompliancePageDomainDialogProp
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{domain.sslStatus === "PENDING" && (
|
{sslStatus === "PENDING" && (
|
||||||
<div className="bg-subtle rounded-lg p-4 mt-4">
|
<div className="bg-subtle rounded-lg p-4 mt-4">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{__(
|
{__(
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ const createCustomDomainMutation = graphql`
|
|||||||
customDomain {
|
customDomain {
|
||||||
id
|
id
|
||||||
domain
|
domain
|
||||||
sslStatus
|
certificate {
|
||||||
|
status
|
||||||
|
expiresAt
|
||||||
|
provisioningError
|
||||||
|
}
|
||||||
dnsRecords {
|
dnsRecords {
|
||||||
type
|
type
|
||||||
name
|
name
|
||||||
@@ -52,7 +56,6 @@ const createCustomDomainMutation = graphql`
|
|||||||
}
|
}
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
sslExpiresAt
|
|
||||||
canDelete: permission(action: "compliance-portal:custom-domain:delete")
|
canDelete: permission(action: "compliance-portal:custom-domain:delete")
|
||||||
...CompliancePageDomainCardFragment
|
...CompliancePageDomainCardFragment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,73 +138,18 @@ func (s *Service) RemoveCustomDomain(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefaultDomain returns the compliance page's default probopage subdomain,
|
// GetDomain returns a custom domain by ID.
|
||||||
// or nil when it has not been provisioned yet.
|
func (s *Service) GetDomain(
|
||||||
func (s *Service) GetDefaultDomain(
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
scope coredata.Scoper,
|
scope coredata.Scoper,
|
||||||
compliancePageID gid.GID,
|
domainID gid.GID,
|
||||||
) (*coredata.CustomDomain, error) {
|
) (*coredata.CustomDomain, error) {
|
||||||
var domain *coredata.CustomDomain
|
domain := &coredata.CustomDomain{}
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
err := s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
func(ctx context.Context, conn pg.Querier) error {
|
func(ctx context.Context, conn pg.Querier) error {
|
||||||
compliancePage := &coredata.TrustCenter{}
|
if err := domain.LoadByID(ctx, conn, scope, domainID); err != nil {
|
||||||
if err := compliancePage.LoadByID(ctx, conn, scope, compliancePageID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load compliance page: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if compliancePage.DefaultDomainID == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
domain = &coredata.CustomDomain{}
|
|
||||||
if err := domain.LoadByID(ctx, conn, scope, *compliancePage.DefaultDomainID); err != nil {
|
|
||||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
||||||
domain = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("cannot load custom domain: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetCustomDomain(
|
|
||||||
ctx context.Context,
|
|
||||||
scope coredata.Scoper,
|
|
||||||
compliancePageID gid.GID,
|
|
||||||
) (*coredata.CustomDomain, error) {
|
|
||||||
var domain *coredata.CustomDomain
|
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
|
||||||
ctx,
|
|
||||||
func(ctx context.Context, conn pg.Querier) error {
|
|
||||||
compliancePage := &coredata.TrustCenter{}
|
|
||||||
if err := compliancePage.LoadByID(ctx, conn, scope, compliancePageID); err != nil {
|
|
||||||
return fmt.Errorf("cannot load compliance page: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if compliancePage.CustomDomainID == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
domain = &coredata.CustomDomain{}
|
|
||||||
if err := domain.LoadByID(ctx, conn, scope, *compliancePage.CustomDomainID); err != nil {
|
|
||||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
|
||||||
domain = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("cannot load custom domain: %w", err)
|
return fmt.Errorf("cannot load custom domain: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -249,31 +249,21 @@ enum TrustCenterFileOrderField
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum SSLStatus
|
enum SSLStatus
|
||||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatus") {
|
@goModel(model: "go.probo.inc/probo/pkg/coredata.CertificateStatus") {
|
||||||
PENDING
|
PENDING
|
||||||
@goEnum(
|
@goEnum(value: "go.probo.inc/probo/pkg/coredata.CertificateStatusPending")
|
||||||
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusPending"
|
|
||||||
)
|
|
||||||
PROVISIONING
|
PROVISIONING
|
||||||
@goEnum(
|
@goEnum(
|
||||||
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusProvisioning"
|
value: "go.probo.inc/probo/pkg/coredata.CertificateStatusProvisioning"
|
||||||
)
|
)
|
||||||
ACTIVE
|
ACTIVE
|
||||||
@goEnum(
|
@goEnum(value: "go.probo.inc/probo/pkg/coredata.CertificateStatusActive")
|
||||||
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusActive"
|
|
||||||
)
|
|
||||||
RENEWING
|
RENEWING
|
||||||
@goEnum(
|
@goEnum(value: "go.probo.inc/probo/pkg/coredata.CertificateStatusRenewing")
|
||||||
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusRenewing"
|
|
||||||
)
|
|
||||||
EXPIRED
|
EXPIRED
|
||||||
@goEnum(
|
@goEnum(value: "go.probo.inc/probo/pkg/coredata.CertificateStatusExpired")
|
||||||
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusExpired"
|
|
||||||
)
|
|
||||||
FAILED
|
FAILED
|
||||||
@goEnum(
|
@goEnum(value: "go.probo.inc/probo/pkg/coredata.CertificateStatusFailed")
|
||||||
value: "go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatusFailed"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input TrustCenterAccessOrder
|
input TrustCenterAccessOrder
|
||||||
@@ -648,9 +638,7 @@ type CustomDomain implements Node {
|
|||||||
organization: Organization!
|
organization: Organization!
|
||||||
domain: String!
|
domain: String!
|
||||||
managed: Boolean!
|
managed: Boolean!
|
||||||
sslStatus: SSLStatus!
|
certificate: Certificate @goField(forceResolver: true)
|
||||||
sslExpiresAt: Datetime
|
|
||||||
provisioningError: String
|
|
||||||
dnsRecords: [DNSRecordInstruction!]!
|
dnsRecords: [DNSRecordInstruction!]!
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
@@ -658,6 +646,13 @@ type CustomDomain implements Node {
|
|||||||
permission(action: String!): Boolean! @goField(forceResolver: true)
|
permission(action: String!): Boolean! @goField(forceResolver: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Certificate {
|
||||||
|
id: ID!
|
||||||
|
status: SSLStatus!
|
||||||
|
expiresAt: Datetime
|
||||||
|
provisioningError: String
|
||||||
|
}
|
||||||
|
|
||||||
type DNSRecordInstruction {
|
type DNSRecordInstruction {
|
||||||
type: String!
|
type: String!
|
||||||
name: String!
|
name: String!
|
||||||
|
|||||||
@@ -85,26 +85,6 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// newCustomDomainType loads the domain's certificate (when present) and builds
|
|
||||||
// the GraphQL CustomDomain type with its certificate-backed SSL fields.
|
|
||||||
func (r *Resolver) newCustomDomainType(
|
|
||||||
ctx context.Context,
|
|
||||||
scope coredata.Scoper,
|
|
||||||
domain *coredata.CustomDomain,
|
|
||||||
) (*types.CustomDomain, error) {
|
|
||||||
var cert *coredata.Certificate
|
|
||||||
if domain != nil && domain.CertificateID != nil {
|
|
||||||
var err error
|
|
||||||
cert, err = r.certManager.Get(ctx, scope, *domain.CertificateID)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot load certificate", log.Error(err))
|
|
||||||
return nil, gqlutils.Internal(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return types.NewCustomDomain(domain, cert, r.customDomainCname), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMux(
|
func NewMux(
|
||||||
logger *log.Logger,
|
logger *log.Logger,
|
||||||
proboSvc *probo.Service,
|
proboSvc *probo.Service,
|
||||||
|
|||||||
@@ -124,6 +124,30 @@ func (r *compliancePortalCommitmentGroupConnectionResolver) TotalCount(ctx conte
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Certificate is the resolver for the certificate field.
|
||||||
|
func (r *customDomainResolver) Certificate(ctx context.Context, obj *types.CustomDomain) (*types.Certificate, error) {
|
||||||
|
if obj.Certificate == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scope, err := r.authorize(ctx, obj.ID, management.ActionCustomDomainGet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := r.certManager.Get(ctx, scope, obj.Certificate.ID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot load certificate", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewCertificate(cert), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Permission is the resolver for the permission field.
|
// Permission is the resolver for the permission field.
|
||||||
func (r *customDomainResolver) Permission(ctx context.Context, obj *types.CustomDomain, action string) (bool, error) {
|
func (r *customDomainResolver) Permission(ctx context.Context, obj *types.CustomDomain, action string) (bool, error) {
|
||||||
return r.Resolver.Permission(ctx, obj, action)
|
return r.Resolver.Permission(ctx, obj, action)
|
||||||
@@ -893,13 +917,8 @@ func (r *mutationResolver) CreateCustomDomain(ctx context.Context, input types.C
|
|||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
customDomain, err := r.newCustomDomainType(ctx, scope, domain)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.CreateCustomDomainPayload{
|
return &types.CreateCustomDomainPayload{
|
||||||
CustomDomain: customDomain,
|
CustomDomain: types.NewCustomDomain(domain, r.customDomainCname),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1169,42 +1188,50 @@ func (r *trustCenterResolver) MailingList(ctx context.Context, obj *types.TrustC
|
|||||||
|
|
||||||
// DefaultDomain is the resolver for the defaultDomain field.
|
// DefaultDomain is the resolver for the defaultDomain field.
|
||||||
func (r *trustCenterResolver) DefaultDomain(ctx context.Context, obj *types.TrustCenter) (*types.CustomDomain, error) {
|
func (r *trustCenterResolver) DefaultDomain(ctx context.Context, obj *types.TrustCenter) (*types.CustomDomain, error) {
|
||||||
|
if obj.DefaultDomain == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
scope, err := r.authorize(ctx, obj.ID, management.ActionCustomDomainGet)
|
scope, err := r.authorize(ctx, obj.ID, management.ActionCustomDomainGet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := r.management.GetDefaultDomain(ctx, scope, obj.ID)
|
domain, err := r.management.GetDomain(ctx, scope, obj.DefaultDomain.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot load default domain", log.Error(err))
|
r.logger.ErrorCtx(ctx, "cannot load default domain", log.Error(err))
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
if domain == nil {
|
return types.NewCustomDomain(domain, r.customDomainCname), nil
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.newCustomDomainType(ctx, scope, domain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CustomDomain is the resolver for the customDomain field.
|
// CustomDomain is the resolver for the customDomain field.
|
||||||
func (r *trustCenterResolver) CustomDomain(ctx context.Context, obj *types.TrustCenter) (*types.CustomDomain, error) {
|
func (r *trustCenterResolver) CustomDomain(ctx context.Context, obj *types.TrustCenter) (*types.CustomDomain, error) {
|
||||||
|
if obj.CustomDomain == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
scope, err := r.authorize(ctx, obj.ID, management.ActionCustomDomainGet)
|
scope, err := r.authorize(ctx, obj.ID, management.ActionCustomDomainGet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := r.management.GetCustomDomain(ctx, scope, obj.ID)
|
domain, err := r.management.GetDomain(ctx, scope, obj.CustomDomain.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot load custom domain", log.Error(err))
|
r.logger.ErrorCtx(ctx, "cannot load custom domain", log.Error(err))
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
if domain == nil {
|
return types.NewCustomDomain(domain, r.customDomainCname), nil
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.newCustomDomainType(ctx, scope, domain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicURL is the resolver for the publicUrl field.
|
// PublicURL is the resolver for the publicUrl field.
|
||||||
|
|||||||
34
pkg/server/api/console/v1/types/certificate.go
Normal file
34
pkg/server/api/console/v1/types/certificate.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCertificate(c *coredata.Certificate) *Certificate {
|
||||||
|
return &Certificate{
|
||||||
|
ID: c.ID,
|
||||||
|
Status: c.Status,
|
||||||
|
ExpiresAt: c.SSLExpiresAt,
|
||||||
|
ProvisioningError: c.ProvisioningError,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,46 +24,34 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewCustomDomain builds the GraphQL CustomDomain type. The TLS lifecycle now
|
func NewCustomDomain(d *coredata.CustomDomain, cnameTarget string) *CustomDomain {
|
||||||
// lives on the linked certificate; when cert is nil (certificate not yet
|
domain := &CustomDomain{
|
||||||
// created) the domain reports a pending SSL status.
|
|
||||||
func NewCustomDomain(d *coredata.CustomDomain, cert *coredata.Certificate, cnameTarget string) *CustomDomain {
|
|
||||||
result := &CustomDomain{
|
|
||||||
ID: d.ID,
|
ID: d.ID,
|
||||||
Organization: &Organization{
|
Organization: &Organization{
|
||||||
ID: d.OrganizationID,
|
ID: d.OrganizationID,
|
||||||
},
|
},
|
||||||
Domain: d.Domain,
|
Domain: d.Domain,
|
||||||
Managed: d.Managed,
|
Managed: d.Managed,
|
||||||
SslStatus: coredata.CustomDomainSSLStatusPending,
|
DNSRecords: convertDNSRecords(d, cnameTarget),
|
||||||
CreatedAt: d.CreatedAt,
|
CreatedAt: d.CreatedAt,
|
||||||
UpdatedAt: d.UpdatedAt,
|
UpdatedAt: d.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
if cert != nil {
|
if d.CertificateID != nil {
|
||||||
result.SslStatus = coredata.CustomDomainSSLStatus(cert.Status)
|
domain.Certificate = &Certificate{ID: *d.CertificateID}
|
||||||
result.SslExpiresAt = cert.SSLExpiresAt
|
|
||||||
result.ProvisioningError = cert.ProvisioningError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert DNS records
|
return domain
|
||||||
result.DNSRecords = convertDNSRecords(d, cnameTarget)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertDNSRecords(d *coredata.CustomDomain, cnameTarget string) []*DNSRecordInstruction {
|
func convertDNSRecords(d *coredata.CustomDomain, cnameTarget string) []*DNSRecordInstruction {
|
||||||
var records []*DNSRecordInstruction
|
return []*DNSRecordInstruction{
|
||||||
|
{
|
||||||
// For HTTP-01 challenges, we just need the domain to point to our servers via CNAME
|
|
||||||
record := &DNSRecordInstruction{
|
|
||||||
Type: "CNAME",
|
Type: "CNAME",
|
||||||
Name: d.Domain,
|
Name: d.Domain,
|
||||||
Value: cnameTarget,
|
Value: cnameTarget,
|
||||||
TTL: 300,
|
TTL: 300,
|
||||||
Purpose: "Point domain to Probo servers",
|
Purpose: "Point domain to Probo servers",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
records = append(records, record)
|
|
||||||
|
|
||||||
return records
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ type TrustCenter struct {
|
|||||||
ComplianceFrameworks *ComplianceFrameworkConnection `json:"complianceFrameworks"`
|
ComplianceFrameworks *ComplianceFrameworkConnection `json:"complianceFrameworks"`
|
||||||
CustomLinks *ComplianceCustomLinkConnection `json:"customLinks"`
|
CustomLinks *ComplianceCustomLinkConnection `json:"customLinks"`
|
||||||
MailingList *MailingList `json:"mailingList,omitempty"`
|
MailingList *MailingList `json:"mailingList,omitempty"`
|
||||||
|
DefaultDomain *CustomDomain `json:"defaultDomain,omitempty"`
|
||||||
|
CustomDomain *CustomDomain `json:"customDomain,omitempty"`
|
||||||
Permission bool `json:"permission"`
|
Permission bool `json:"permission"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,5 +84,13 @@ func NewTrustCenter(tc *coredata.TrustCenter) *TrustCenter {
|
|||||||
trustCenter.Nda = &File{ID: *tc.NonDisclosureAgreementFileID}
|
trustCenter.Nda = &File{ID: *tc.NonDisclosureAgreementFileID}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tc.DefaultDomainID != nil {
|
||||||
|
trustCenter.DefaultDomain = &CustomDomain{ID: *tc.DefaultDomainID}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.CustomDomainID != nil {
|
||||||
|
trustCenter.CustomDomain = &CustomDomain{ID: *tc.CustomDomainID}
|
||||||
|
}
|
||||||
|
|
||||||
return trustCenter
|
return trustCenter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5306,15 +5306,20 @@ func (r *Resolver) DeleteCustomDomainTool(ctx context.Context, req *mcp.CallTool
|
|||||||
return nil, types.DeleteCustomDomainOutput{}, err
|
return nil, types.DeleteCustomDomainOutput{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := r.management.GetCustomDomain(ctx, scope, input.TrustCenterID)
|
compliancePage, err := r.management.Get(ctx, scope, input.TrustCenterID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, types.DeleteCustomDomainOutput{}, fmt.Errorf("cannot get custom domain: %w", err)
|
return nil, types.DeleteCustomDomainOutput{}, fmt.Errorf("cannot load compliance page: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if domain == nil {
|
if compliancePage.CustomDomainID == nil {
|
||||||
return nil, types.DeleteCustomDomainOutput{}, fmt.Errorf("compliance page has no custom domain")
|
return nil, types.DeleteCustomDomainOutput{}, fmt.Errorf("compliance page has no custom domain")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
domain, err := r.management.GetDomain(ctx, scope, *compliancePage.CustomDomainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.DeleteCustomDomainOutput{}, fmt.Errorf("cannot get custom domain: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var cert *coredata.Certificate
|
var cert *coredata.Certificate
|
||||||
if domain.CertificateID != nil {
|
if domain.CertificateID != nil {
|
||||||
cert, err = r.certManager.Get(ctx, scope, *domain.CertificateID)
|
cert, err = r.certManager.Get(ctx, scope, *domain.CertificateID)
|
||||||
|
|||||||
@@ -8737,7 +8737,7 @@ components:
|
|||||||
- RENEWING
|
- RENEWING
|
||||||
- EXPIRED
|
- EXPIRED
|
||||||
- FAILED
|
- FAILED
|
||||||
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatus
|
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.CertificateStatus
|
||||||
|
|
||||||
TrustCenterReferenceOrderField:
|
TrustCenterReferenceOrderField:
|
||||||
type: string
|
type: string
|
||||||
@@ -8968,6 +8968,26 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
|
||||||
|
Certificate:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- status
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
status:
|
||||||
|
$ref: "#/components/schemas/SSLStatus"
|
||||||
|
expires_at:
|
||||||
|
anyOf:
|
||||||
|
- type: string
|
||||||
|
format: date-time
|
||||||
|
- type: "null"
|
||||||
|
provisioning_error:
|
||||||
|
anyOf:
|
||||||
|
- type: string
|
||||||
|
- type: "null"
|
||||||
|
|
||||||
CustomDomain:
|
CustomDomain:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -8975,7 +8995,6 @@ components:
|
|||||||
- organization_id
|
- organization_id
|
||||||
- domain
|
- domain
|
||||||
- managed
|
- managed
|
||||||
- ssl_status
|
|
||||||
- created_at
|
- created_at
|
||||||
- updated_at
|
- updated_at
|
||||||
properties:
|
properties:
|
||||||
@@ -8988,12 +9007,9 @@ components:
|
|||||||
managed:
|
managed:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Whether this domain is a Probo-managed probopage subdomain
|
description: Whether this domain is a Probo-managed probopage subdomain
|
||||||
ssl_status:
|
certificate:
|
||||||
$ref: "#/components/schemas/SSLStatus"
|
|
||||||
ssl_expires_at:
|
|
||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- $ref: "#/components/schemas/Certificate"
|
||||||
format: date-time
|
|
||||||
- type: "null"
|
- type: "null"
|
||||||
created_at:
|
created_at:
|
||||||
type: string
|
type: string
|
||||||
|
|||||||
@@ -24,24 +24,24 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewCustomDomain builds the MCP CustomDomain type. The TLS lifecycle now lives
|
|
||||||
// on the linked certificate; when cert is nil (certificate not yet created) the
|
|
||||||
// domain reports a pending SSL status.
|
|
||||||
func NewCustomDomain(d *coredata.CustomDomain, cert *coredata.Certificate) *CustomDomain {
|
func NewCustomDomain(d *coredata.CustomDomain, cert *coredata.Certificate) *CustomDomain {
|
||||||
result := &CustomDomain{
|
domain := &CustomDomain{
|
||||||
ID: d.ID,
|
ID: d.ID,
|
||||||
OrganizationID: d.OrganizationID,
|
OrganizationID: d.OrganizationID,
|
||||||
Domain: d.Domain,
|
Domain: d.Domain,
|
||||||
Managed: d.Managed,
|
Managed: d.Managed,
|
||||||
SslStatus: coredata.CustomDomainSSLStatusPending,
|
|
||||||
CreatedAt: d.CreatedAt,
|
CreatedAt: d.CreatedAt,
|
||||||
UpdatedAt: d.UpdatedAt,
|
UpdatedAt: d.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
if cert != nil {
|
if cert != nil {
|
||||||
result.SslStatus = coredata.CustomDomainSSLStatus(cert.Status)
|
domain.Certificate = &Certificate{
|
||||||
result.SslExpiresAt = cert.SSLExpiresAt
|
ID: cert.ID,
|
||||||
|
Status: cert.Status,
|
||||||
|
ExpiresAt: cert.SSLExpiresAt,
|
||||||
|
ProvisioningError: cert.ProvisioningError,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return domain
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user