Rewire the console and visitor resolvers onto the management and visitor services with compliance-portal authorization. Rename the GraphQL and MCP ComplianceExternalURL type to ComplianceCustomLink, expose trust center profile fields, default and custom domains, public URL, and the managed flag, and drop the profile fields from the organization surface. Signed-off-by: Bryan Frimin <bryan@probo.com>
216 lines
6.8 KiB
Go
216 lines
6.8 KiB
Go
package trust_v1
|
|
|
|
// This file will be automatically regenerated based on the schema, any resolver
|
|
// implementations
|
|
// will be copied through when generating and any unknown code will be moved to the end.
|
|
// Code generated by github.com/99designs/gqlgen version v0.17.93
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"go.gearno.de/kit/log"
|
|
"go.probo.inc/probo/pkg/baseurl"
|
|
"go.probo.inc/probo/pkg/coredata"
|
|
"go.probo.inc/probo/pkg/iam"
|
|
"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/gqlutils"
|
|
)
|
|
|
|
// SendMagicLink is the resolver for the sendMagicLink field.
|
|
func (r *mutationResolver) SendMagicLink(ctx context.Context, input types.SendMagicLinkInput) (*types.SendMagicLinkPayload, error) {
|
|
trustCenter := complianceportal.CompliancePageFromContext(ctx)
|
|
|
|
baseURL := complianceportal.CompliancePageBaseURLFromContext(ctx)
|
|
|
|
safeRedirect := saferedirect.New(saferedirect.StaticHosts(baseurl.MustParse(*baseURL).Host()))
|
|
|
|
if input.Continue != nil {
|
|
_, ok := safeRedirect.Validate(ctx, *input.Continue)
|
|
if !ok {
|
|
return nil, gqlutils.Invalidf(ctx, "invalid continue URL")
|
|
}
|
|
}
|
|
|
|
req := &iam.SendMagicLinkRequest{
|
|
Email: input.Email,
|
|
CompliancePageID: &trustCenter.ID,
|
|
OrganizationID: trustCenter.OrganizationID,
|
|
URLPath: "verify-magic-link",
|
|
Continue: input.Continue,
|
|
}
|
|
|
|
if err := r.iam.AuthService.SendMagicLink(ctx, req); err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot send magic link", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// VerifyMagicLink is the resolver for the verifyMagicLink field.
|
|
func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.VerifyMagicLinkInput) (*types.VerifyMagicLinkPayload, error) {
|
|
session := authn.SessionFromContext(ctx)
|
|
identity := authn.IdentityFromContext(ctx)
|
|
|
|
email, err := r.iam.AuthService.GetMagicLinkEmail(ctx, input.Token)
|
|
if err != nil {
|
|
if _, ok := errors.AsType[*iam.ErrExpiredToken](err); ok {
|
|
return nil, gqlutils.TokenExpired(ctx, err)
|
|
}
|
|
|
|
if _, ok := errors.AsType[*iam.ErrInvalidToken](err); ok {
|
|
return nil, gqlutils.Invalid(ctx, err)
|
|
}
|
|
|
|
r.logger.ErrorCtx(ctx, "cannot get magic link email", log.Error(err))
|
|
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
var continueURL *string
|
|
|
|
switch {
|
|
case session == nil:
|
|
var err error
|
|
|
|
identity, session, continueURL, err = r.iam.AuthService.OpenSessionWithMagicLink(ctx, input.Token)
|
|
if err != nil {
|
|
if _, ok := errors.AsType[*iam.ErrExpiredToken](err); ok {
|
|
return nil, gqlutils.TokenExpired(ctx, err)
|
|
}
|
|
|
|
if _, ok := errors.AsType[*iam.ErrTokenAlreadyUsed](err); ok {
|
|
return nil, gqlutils.TokenAlreadyUsed(ctx, err)
|
|
}
|
|
|
|
if _, ok := errors.AsType[*iam.ErrInvalidToken](err); ok {
|
|
return nil, gqlutils.Invalid(ctx, err)
|
|
}
|
|
|
|
r.logger.ErrorCtx(ctx, "cannot open session with magic link", log.Error(err))
|
|
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
case identity.EmailAddress != email:
|
|
if err := r.iam.SessionService.CloseSession(ctx, session.ID); err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot close session", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
var err error
|
|
|
|
identity, session, continueURL, err = r.iam.AuthService.OpenSessionWithMagicLink(ctx, input.Token)
|
|
if err != nil {
|
|
if _, ok := errors.AsType[*iam.ErrExpiredToken](err); ok {
|
|
return nil, gqlutils.TokenExpired(ctx, err)
|
|
}
|
|
|
|
if _, ok := errors.AsType[*iam.ErrTokenAlreadyUsed](err); ok {
|
|
return nil, gqlutils.TokenAlreadyUsed(ctx, err)
|
|
}
|
|
|
|
if _, ok := errors.AsType[*iam.ErrInvalidToken](err); ok {
|
|
return nil, gqlutils.Invalid(ctx, err)
|
|
}
|
|
|
|
r.logger.ErrorCtx(ctx, "cannot open session with magic link", log.Error(err))
|
|
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
}
|
|
|
|
trustCenter := complianceportal.CompliancePageFromContext(ctx)
|
|
|
|
if _, err := r.trust.ProvisionPortalMember(ctx, trustCenter.ID, identity.ID); err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot provision member", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
w := gqlutils.HTTPResponseWriterFromContext(ctx)
|
|
r.sessionCookie.Set(w, session)
|
|
|
|
return &types.VerifyMagicLinkPayload{
|
|
Continue: continueURL,
|
|
}, nil
|
|
}
|
|
|
|
// UpdateFullName is the resolver for the updateFullName field.
|
|
func (r *mutationResolver) UpdateFullName(ctx context.Context, input types.UpdateFullNameInput) (*types.UpdateFullNamePayload, error) {
|
|
identity := authn.IdentityFromContext(ctx)
|
|
if identity == nil {
|
|
return nil, gqlutils.Unauthenticatedf(ctx, "authentication is required to request access")
|
|
}
|
|
|
|
identity, err := r.iam.AccountService.UpdateIdentity(
|
|
ctx,
|
|
identity.ID,
|
|
&iam.UpdateIdentityRequest{
|
|
FullName: input.FullName,
|
|
},
|
|
)
|
|
if err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot update identity", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
compliancePage := complianceportal.CompliancePageFromContext(ctx)
|
|
|
|
profile, err := r.iam.OrganizationService.GetProfileForIdentityAndOrganization(ctx, identity.ID, compliancePage.OrganizationID)
|
|
if err != nil {
|
|
// External trust-center visitors have no organization profile; updating
|
|
// the identity's full name above is all that is needed for them.
|
|
if _, ok := errors.AsType[*iam.ErrProfileNotFound](err); ok {
|
|
return &types.UpdateFullNamePayload{Success: true}, nil
|
|
}
|
|
|
|
r.logger.ErrorCtx(ctx, "cannot get profile", log.Error(err))
|
|
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
if profile.Source == coredata.ProfileSourceManual {
|
|
if _, err := r.iam.OrganizationService.UpdateUser(ctx, &iam.UpdateUserRequest{
|
|
ID: profile.ID,
|
|
FullName: identity.FullName,
|
|
AdditionalEmailAddresses: profile.AdditionalEmailAddresses,
|
|
Kind: profile.Kind,
|
|
Position: profile.Position,
|
|
ContractStartDate: &profile.ContractStartDate,
|
|
ContractEndDate: &profile.ContractEndDate,
|
|
}); err != nil {
|
|
r.logger.ErrorCtx(ctx, "cannot update profile", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
}
|
|
|
|
return &types.UpdateFullNamePayload{Success: true}, nil
|
|
}
|
|
|
|
// SignOut is the resolver for the signOut field.
|
|
func (r *mutationResolver) SignOut(ctx context.Context) (*types.SignOutPayload, error) {
|
|
session := authn.SessionFromContext(ctx)
|
|
|
|
err := r.iam.SessionService.CloseSession(ctx, session.ID)
|
|
if err != nil {
|
|
_, notFound := errors.AsType[*iam.ErrSessionNotFound](err)
|
|
|
|
_, expired := errors.AsType[*iam.ErrSessionExpired](err)
|
|
if !notFound && !expired {
|
|
r.logger.ErrorCtx(ctx, "cannot close session", log.Error(err))
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
// Already closed or missing — still clear the cookie so the browser
|
|
// drops the stale session on concurrent / retried logout.
|
|
}
|
|
|
|
w := gqlutils.HTTPResponseWriterFromContext(ctx)
|
|
r.sessionCookie.Clear(w)
|
|
|
|
return &types.SignOutPayload{Success: true}, nil
|
|
}
|