Files
probo/pkg/server/api/complianceportal/v1/auth_resolvers.go
Bryan Frimin 31157ff2e3 Move trust GraphQL API under complianceportal v1
Relocate the public trust center GraphQL surface, OAuth handlers,
and SPA serving into the compliance portal API package and remove
the legacy trust v1 server tree.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-07-21 15:44:12 +02:00

232 lines
7.3 KiB
Go

package complianceportal_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/complianceportal/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)
}
}
req := gqlutils.HTTPRequestFromContext(ctx)
if req == nil {
return nil, gqlutils.Internal(ctx)
}
host, ok := complianceportal.TrustedRequestHost(req)
if !ok {
return nil, gqlutils.Internal(ctx)
}
session.Data = coredata.SessionDataForHost(host)
if err := r.iam.SessionService.UpdateSessionData(ctx, session.ID, session.Data); err != nil {
r.logger.ErrorCtx(ctx, "cannot bind session to host", 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
}