Route the full-name and NDA gates from the request-access flows to their gate pages (deep-linking with the deferred continue URL) instead of a dead-end toast, so signing or naming resumes the original request; the shared gate-to-route mapping now lives in one helper reused by the route boundaries and both request hooks. Fix the NDA page redirecting to home while also redirecting to the continue URL once the signature is sealed, surface consent/accept failures so the sign button isn't silently inert, and build the request-all continue URL before clearing its marker. On the backend, return success from updateFullName when the identity has no organization profile instead of dereferencing a nil profile, which crashed external trust-center visitors completing the full-name gate. Signed-off-by: Émile Ré <emile@probo.com>
192 lines
6.1 KiB
Go
192 lines
6.1 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/compliancepage"
|
|
"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 := compliancepage.CompliancePageFromContext(ctx)
|
|
|
|
baseURL := compliancepage.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 := compliancepage.CompliancePageFromContext(ctx)
|
|
|
|
if _, err := r.trust.ProvisionMember(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 := compliancepage.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
|
|
}
|