Add magic link login for trust center

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-01-15 12:31:10 +04:00
committed by Bryan Frimin
parent 8ff4693bfc
commit 8b3bda56e6
42 changed files with 1880 additions and 993 deletions

View File

@@ -13,6 +13,7 @@ import (
"time"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
@@ -84,7 +85,7 @@ func (r *documentResolver) IsUserAuthorized(ctx context.Context, obj *types.Docu
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return false, gqlutils.Unauthenticatedf(ctx, "unauthenticated")
return false, nil
}
documentAccess, err := trustService.TrustCenterAccesses.LoadDocumentAccess(
@@ -143,8 +144,68 @@ func (r *frameworkResolver) DarkLogoURL(ctx context.Context, obj *types.Framewor
return trustService.Frameworks.GenerateDarkLogoURL(ctx, obj.ID, 1*time.Hour)
}
// SignInWithToken is the resolver for the signInWithToken field.
func (r *mutationResolver) SignInWithToken(ctx context.Context, input types.SignInWithTokenInput) (*types.SignInWithTokenPayload, error) {
// SendMagicLink is the resolver for the sendMagicLink field.
func (r *mutationResolver) SendMagicLink(ctx context.Context, input types.SendMagicLinkInput) (*types.SendMagicLinkPayload, error) {
identity := authn.IdentityFromContext(ctx)
if identity != nil {
return nil, gqlutils.AlreadyAuthenticatedf(ctx, "already authenticated")
}
trustCenter := compliancepage.CompliancePageFromContext(ctx)
organization, err := r.iam.OrganizationService.GetOrganization(ctx, trustCenter.OrganizationID)
if err != nil {
var errNotFound *iam.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFoundf(ctx, "organization not found")
}
r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
customDomain, err := r.trust.GetCustomDomainByOrganizationID(ctx, organization.ID)
if err != nil {
var errNotFound *iam.ErrOrganizationNotFound
if !errors.As(err, &errNotFound) {
r.logger.ErrorCtx(ctx, "cannot get custom domain", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
}
req := &iam.SendMagicLinkRequest{
Email: input.Email,
}
if false {
// if customDomain != nil {
baseURL, err := baseurl.Parse(fmt.Sprintf("https://%s", customDomain.Domain))
if err != nil {
r.logger.ErrorCtx(ctx, "cannot parse custom domain url", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
req.BaseURL = baseURL
} else {
baseURL, err := baseurl.Parse(r.baseURL.WithPath("/trust/" + trustCenter.ID.String()).MustString())
if err != nil {
r.logger.ErrorCtx(ctx, "cannot parse url", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
req.BaseURL = baseURL
}
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, err := r.iam.AuthService.OpenSessionWithMagicLink(ctx, input.Token)
if err != nil {
var errInvalidToken *iam.ErrInvalidToken
@@ -156,33 +217,23 @@ func (r *mutationResolver) SignInWithToken(ctx context.Context, input types.Sign
return nil, gqlutils.Internal(ctx)
}
// TODO cookie domain
// FIXME: cookie domain
w := gqlutils.HTTPResponseWriterFromContext(ctx)
r.sessionCookie.Set(w, session)
return &types.SignInWithTokenPayload{
return &types.VerifyMagicLinkPayload{
Success: true,
}, nil
}
// RequestAllAccesses is the resolver for the requestAllAccesses field.
func (r *mutationResolver) RequestAllAccesses(ctx context.Context, input types.RequestAllAccessesInput) (*types.RequestAccessesPayload, error) {
func (r *mutationResolver) RequestAllAccesses(ctx context.Context) (*types.RequestAccessesPayload, error) {
trustCenter := compliancepage.CompliancePageFromContext(ctx)
trustService := r.TrustService(ctx, trustCenter.ID.TenantID())
identity := authn.IdentityFromContext(ctx)
if identity == nil {
var err error
identity, err = r.iam.AuthService.LoadOrCreateIdentity(
ctx,
&iam.LoadOrCreateIdentityRequest{
Email: input.Email,
},
)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot load or create identity", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return nil, gqlutils.Unauthenticatedf(ctx, "authentication is required to request access")
}
access, err := trustService.TrustCenterAccesses.Request(
@@ -242,10 +293,6 @@ func (r *mutationResolver) ExportDocumentPDF(ctx context.Context, input types.Ex
ndaExists := true
hasAcceptedNDA := false
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get trust center", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
if trustCenter.NonDisclosureAgreementFileID == nil {
ndaExists = false
}
@@ -296,6 +343,11 @@ func (r *mutationResolver) ExportDocumentPDF(ctx context.Context, input types.Ex
// ExportReportPDF is the resolver for the exportReportPDF field.
func (r *mutationResolver) ExportReportPDF(ctx context.Context, input types.ExportReportPDFInput) (*types.ExportReportPDFPayload, error) {
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated")
}
trustService := r.TrustService(ctx, input.ReportID.TenantID())
trustCenter := compliancepage.CompliancePageFromContext(ctx)
@@ -318,18 +370,9 @@ func (r *mutationResolver) ExportReportPDF(ctx context.Context, input types.Expo
}, nil
}
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated")
}
ndaExists := true
hasAcceptedNDA := false
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get trust center", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
if trustCenter.NonDisclosureAgreementFileID == nil {
ndaExists = false
}
@@ -380,13 +423,12 @@ func (r *mutationResolver) ExportReportPDF(ctx context.Context, input types.Expo
// AcceptNonDisclosureAgreement is the resolver for the acceptNonDisclosureAgreement field.
func (r *mutationResolver) AcceptNonDisclosureAgreement(ctx context.Context) (*types.AcceptNonDisclosureAgreementPayload, error) {
trustCenter := compliancepage.CompliancePageFromContext(ctx)
trustService := r.TrustService(ctx, trustCenter.ID.TenantID())
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated")
}
trustCenter := compliancepage.CompliancePageFromContext(ctx)
trustService := r.TrustService(ctx, trustCenter.ID.TenantID())
httpReq := gqlutils.HTTPRequestFromContext(ctx)
@@ -424,17 +466,7 @@ func (r *mutationResolver) RequestDocumentAccess(ctx context.Context, input type
identity := authn.IdentityFromContext(ctx)
if identity == nil {
var err error
identity, err = r.iam.AuthService.LoadOrCreateIdentity(
ctx,
&iam.LoadOrCreateIdentityRequest{
Email: input.Email,
},
)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot load or create identity", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return nil, gqlutils.Unauthenticatedf(ctx, "authentication is required to request access")
}
access, err := trustService.TrustCenterAccesses.Request(
@@ -483,17 +515,7 @@ func (r *mutationResolver) RequestReportAccess(ctx context.Context, input types.
identity := authn.IdentityFromContext(ctx)
if identity == nil {
var err error
identity, err = r.iam.AuthService.LoadOrCreateIdentity(
ctx,
&iam.LoadOrCreateIdentityRequest{
Email: input.Email,
},
)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot load or create identity", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return nil, gqlutils.Unauthenticatedf(ctx, "authentication is required to request access")
}
access, err := trustService.TrustCenterAccesses.Request(
@@ -542,17 +564,7 @@ func (r *mutationResolver) RequestTrustCenterFileAccess(ctx context.Context, inp
identity := authn.IdentityFromContext(ctx)
if identity == nil {
var err error
identity, err = r.iam.AuthService.LoadOrCreateIdentity(
ctx,
&iam.LoadOrCreateIdentityRequest{
Email: input.Email,
},
)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot load or create identity", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return nil, gqlutils.Unauthenticatedf(ctx, "authentication is required to request access")
}
access, err := trustService.TrustCenterAccesses.Request(
@@ -584,6 +596,11 @@ func (r *mutationResolver) RequestTrustCenterFileAccess(ctx context.Context, inp
// ExportTrustCenterFile is the resolver for the exportTrustCenterFile field.
func (r *mutationResolver) ExportTrustCenterFile(ctx context.Context, input types.ExportTrustCenterFileInput) (*types.ExportTrustCenterFilePayload, error) {
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated")
}
trustCenter := compliancepage.CompliancePageFromContext(ctx)
trustService := r.TrustService(ctx, trustCenter.ID.TenantID())
@@ -605,11 +622,6 @@ func (r *mutationResolver) ExportTrustCenterFile(ctx context.Context, input type
}, nil
}
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated")
}
ndaExists := true
hasAcceptedNDA := false
@@ -782,6 +794,11 @@ func (r *queryResolver) CurrentTrustCenter(ctx context.Context) (*types.TrustCen
// IsUserAuthorized is the resolver for the isUserAuthorized field.
func (r *reportResolver) IsUserAuthorized(ctx context.Context, obj *types.Report) (bool, error) {
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return false, nil
}
trustService := r.TrustService(ctx, obj.ID.TenantID())
trustCenter := compliancepage.CompliancePageFromContext(ctx)
@@ -796,11 +813,6 @@ func (r *reportResolver) IsUserAuthorized(ctx context.Context, obj *types.Report
return true, nil
}
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return false, gqlutils.Unauthenticatedf(ctx, "unauthenticated")
}
reportAccess, err := trustService.TrustCenterAccesses.LoadReportAccess(ctx,
trustCenter.ID,
identity.EmailAddress,
@@ -984,6 +996,11 @@ func (r *trustCenterResolver) TrustCenterFiles(ctx context.Context, obj *types.T
// IsUserAuthorized is the resolver for the isUserAuthorized field.
func (r *trustCenterFileResolver) IsUserAuthorized(ctx context.Context, obj *types.TrustCenterFile) (bool, error) {
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return false, nil
}
trustService := r.TrustService(ctx, obj.ID.TenantID())
trustCenter := compliancepage.CompliancePageFromContext(ctx)
@@ -998,11 +1015,6 @@ func (r *trustCenterFileResolver) IsUserAuthorized(ctx context.Context, obj *typ
return true, nil
}
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return false, gqlutils.Unauthenticatedf(ctx, "unauthenticated")
}
fileAccess, err := trustService.TrustCenterAccesses.LoadTrustCenterFileAccess(ctx,
trustCenter.ID,
identity.EmailAddress,