diff --git a/apps/console/src/__generated__/core/CompliancePageAccessPageQuery.graphql.ts b/apps/console/src/__generated__/core/CompliancePageAccessPageQuery.graphql.ts index 5b21c27bf..c7870e0c6 100644 --- a/apps/console/src/__generated__/core/CompliancePageAccessPageQuery.graphql.ts +++ b/apps/console/src/__generated__/core/CompliancePageAccessPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<35d27a50dd067775566c69dd0e555b25>> * @lightSyntaxTransform * @nogrep */ @@ -17,7 +17,6 @@ export type CompliancePageAccessPageQuery$data = { readonly organization: { readonly __typename: "Organization"; readonly compliancePage: { - readonly canCreateAccess: boolean; readonly id: string; readonly " $fragmentSpreads": FragmentRefs<"CompliancePageAccessListFragment">; }; @@ -61,20 +60,7 @@ v3 = { "name": "id", "storageKey": null }, -v4 = { - "alias": "canCreateAccess", - "args": [ - { - "kind": "Literal", - "name": "action", - "value": "core:trust-center-access:create" - } - ], - "kind": "ScalarField", - "name": "permission", - "storageKey": "permission(action:\"core:trust-center-access:create\")" -}, -v5 = [ +v4 = [ { "kind": "Literal", "name": "first", @@ -119,7 +105,6 @@ return { "plural": false, "selections": [ (v3/*: any*/), - (v4/*: any*/), { "args": null, "kind": "FragmentSpread", @@ -168,10 +153,9 @@ return { "plural": false, "selections": [ (v3/*: any*/), - (v4/*: any*/), { "alias": null, - "args": (v5/*: any*/), + "args": (v4/*: any*/), "concreteType": "TrustCenterAccessConnection", "kind": "LinkedField", "name": "accesses", @@ -350,7 +334,7 @@ return { }, { "alias": null, - "args": (v5/*: any*/), + "args": (v4/*: any*/), "filters": [ "orderBy" ], @@ -373,16 +357,16 @@ return { ] }, "params": { - "cacheID": "c556c1f73c80950e432e5298b7aa1162", + "cacheID": "b4d6f7aa127d6ee60ec6344fb291ab4e", "id": null, "metadata": {}, "name": "CompliancePageAccessPageQuery", "operationKind": "query", - "text": "query CompliancePageAccessPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n compliancePage: trustCenter {\n id\n canCreateAccess: permission(action: \"core:trust-center-access:create\")\n ...CompliancePageAccessListFragment\n }\n }\n id\n }\n}\n\nfragment CompliancePageAccessListFragment on TrustCenter {\n accesses(first: 10, orderBy: {field: CREATED_AT, direction: DESC}) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n ...CompliancePageAccessListItemFragment\n __typename\n }\n }\n }\n id\n}\n\nfragment CompliancePageAccessListItemFragment on TrustCenterAccess {\n id\n createdAt\n profile {\n fullName\n emailAddress\n state\n id\n }\n activeCount\n pendingRequestCount\n ndaSignature {\n status\n id\n }\n canUpdate: permission(action: \"core:trust-center-access:update\")\n}\n" + "text": "query CompliancePageAccessPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n compliancePage: trustCenter {\n id\n ...CompliancePageAccessListFragment\n }\n }\n id\n }\n}\n\nfragment CompliancePageAccessListFragment on TrustCenter {\n accesses(first: 10, orderBy: {field: CREATED_AT, direction: DESC}) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n ...CompliancePageAccessListItemFragment\n __typename\n }\n }\n }\n id\n}\n\nfragment CompliancePageAccessListItemFragment on TrustCenterAccess {\n id\n createdAt\n profile {\n fullName\n emailAddress\n state\n id\n }\n activeCount\n pendingRequestCount\n ndaSignature {\n status\n id\n }\n canUpdate: permission(action: \"core:trust-center-access:update\")\n}\n" } }; })(); -(node as any).hash = "4cc473e91fd49ddd83b6cdb0c8da0abf"; +(node as any).hash = "29f59aac000603a1d11c88a441fb595b"; export default node; diff --git a/apps/trust/src/components/RootErrorBoundary.tsx b/apps/trust/src/components/RootErrorBoundary.tsx index 182da8f30..49724072c 100644 --- a/apps/trust/src/components/RootErrorBoundary.tsx +++ b/apps/trust/src/components/RootErrorBoundary.tsx @@ -1,4 +1,4 @@ -import { NDASignatureRequiredError, UnAuthenticatedError } from "@probo/relay"; +import { FullNameRequiredError, NDASignatureRequiredError, UnAuthenticatedError } from "@probo/relay"; import { Navigate, useLocation, useRouteError } from "react-router"; import { getPathPrefix } from "#/utils/pathPrefix"; @@ -29,6 +29,18 @@ export function RootErrorBoundary() { ); } + if (error instanceof FullNameRequiredError) { + return ( + + ); + } + if (error instanceof NDASignatureRequiredError) { return ( { - const input: SendMagicLinkInput = { email, fullName }; + const handleSubmit = handleSubmitWrapper(({ email }: FormData) => { + const input: SendMagicLinkInput = { email }; if (safeContinueUrl) { input.continue = safeContinueUrl; } @@ -121,7 +119,6 @@ export function ConnectPage(props: { variables: { input: { email, - fullName, continue: safeContinueUrl, }, }, @@ -173,14 +170,6 @@ export function ConnectPage(props: {
void handleSubmit(e)} className="space-y-6"> - ; + +export default function FullNamePage() { + const { __ } = useTranslate(); + const { toast } = useToast(); + const [searchParams] = useSearchParams(); + + const continueUrlParam = searchParams.get("continue"); + let safeContinueUrl: string; + if (continueUrlParam) { + try { + const continueUrl = new URL(continueUrlParam, window.location.origin); + if (continueUrl.origin === window.location.origin && continueUrl.pathname.startsWith(`${getPathPrefix()}/`)) { + safeContinueUrl = window.location.origin + continueUrl.pathname + continueUrl.search; + } else { + safeContinueUrl = window.location.origin + (getPathPrefix() || "/"); + } + } catch { + safeContinueUrl = window.location.origin + (getPathPrefix() || "/"); + } + } else { + safeContinueUrl = window.location.origin + (getPathPrefix() || "/"); + } + + const { + handleSubmit: handleSubmitWrapper, + register, + formState, + } = useFormWithSchema(schema, { + defaultValues: { + fullName: "", + }, + }); + + const [update] = useMutation( + updateMutation, + ); + + const handleSubmit = handleSubmitWrapper(({ fullName }: FormData) => { + update({ + variables: { + input: { + fullName, + }, + }, + onCompleted: (_, errors: GraphQLError[] | null) => { + if (errors) { + for (const err of errors) { + if (err.extensions?.code === "ALREADY_AUTHENTICATED") { + window.location.href = getPathPrefix() || "/"; + return; + } + } + toast({ + title: __("Error"), + description: __("Cannot send magic link"), + variant: "error", + }); + return; + } + + toast({ + title: __("Success"), + description: __("Full name updated!"), + variant: "success", + }); + + window.location.href = safeContinueUrl; + }, + onError: (error) => { + toast({ + title: __("Error"), + description: error.message, + variant: "error", + }); + }, + }); + }); + + return ( +
+
+

+ {__("Please set your profile's full name")} +

+
+ + void handleSubmit(e)} className="space-y-6"> + + + + +
+ ); +} diff --git a/apps/trust/src/pages/auth/__generated__/ConnectPageMutation.graphql.ts b/apps/trust/src/pages/auth/__generated__/ConnectPageMutation.graphql.ts index 453c56541..d358edd83 100644 --- a/apps/trust/src/pages/auth/__generated__/ConnectPageMutation.graphql.ts +++ b/apps/trust/src/pages/auth/__generated__/ConnectPageMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<711ecaa392c23004a3bd1dfb24a5751f>> * @lightSyntaxTransform * @nogrep */ @@ -12,7 +12,6 @@ import { ConcreteRequest } from 'relay-runtime'; export type SendMagicLinkInput = { continue?: string | null | undefined; email: any; - fullName: string; }; export type ConnectPageMutation$variables = { input: SendMagicLinkInput; diff --git a/apps/trust/src/pages/auth/__generated__/FullNamePageMutation.graphql.ts b/apps/trust/src/pages/auth/__generated__/FullNamePageMutation.graphql.ts new file mode 100644 index 000000000..97aa5b84b --- /dev/null +++ b/apps/trust/src/pages/auth/__generated__/FullNamePageMutation.graphql.ts @@ -0,0 +1,92 @@ +/** + * @generated SignedSource<<7861673355d6d647183c2a4425e9a5b9>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type UpdateFullNameInput = { + fullName: string; +}; +export type FullNamePageMutation$variables = { + input: UpdateFullNameInput; +}; +export type FullNamePageMutation$data = { + readonly updateFullName: { + readonly success: boolean; + } | null | undefined; +}; +export type FullNamePageMutation = { + response: FullNamePageMutation$data; + variables: FullNamePageMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UpdateFullNamePayload", + "kind": "LinkedField", + "name": "updateFullName", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "success", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "FullNamePageMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "FullNamePageMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "502db67e5bab72aff71fec9a0be0eeaf", + "id": null, + "metadata": {}, + "name": "FullNamePageMutation", + "operationKind": "mutation", + "text": "mutation FullNamePageMutation(\n $input: UpdateFullNameInput!\n) {\n updateFullName(input: $input) {\n success\n }\n}\n" + } +}; +})(); + +(node as any).hash = "3d6352889babf98a37fe06a1bcaead15"; + +export default node; diff --git a/apps/trust/src/providers/RelayProviders.tsx b/apps/trust/src/providers/RelayProviders.tsx index 7ffae5d3c..3c28d0932 100644 --- a/apps/trust/src/providers/RelayProviders.tsx +++ b/apps/trust/src/providers/RelayProviders.tsx @@ -10,32 +10,6 @@ import { import { getPathPrefix } from "#/utils/pathPrefix"; -export class UnAuthenticatedError extends Error { - constructor() { - super("UNAUTHENTICATED"); - this.name = "UnAuthenticatedError"; - } -} - -export class InvalidError extends Error { - field?: string; - cause?: string; - - constructor(message?: string, field?: string, cause?: string) { - super(message || "INVALID"); - this.name = "InvalidError"; - this.field = field; - this.cause = cause; - } -} - -export class InternalServerError extends Error { - constructor() { - super("INTERNAL_SERVER_ERROR"); - this.name = "InternalServerError"; - } -} - export function buildEndpoint(): string { let host = import.meta.env.VITE_API_URL; diff --git a/apps/trust/src/routes.tsx b/apps/trust/src/routes.tsx index 0401cec4d..e99c49b91 100644 --- a/apps/trust/src/routes.tsx +++ b/apps/trust/src/routes.tsx @@ -37,6 +37,10 @@ const routes = [ path: "/verify-magic-link", Component: lazy(() => import("#/pages/auth/VerifyMagicLinkPage")), }, + { + path: "/full-name", + Component: lazy(() => import("#/pages/auth/FullNamePage")), + }, ], }, { diff --git a/packages/relay/src/errors.ts b/packages/relay/src/errors.ts index 450979eb8..de48206af 100644 --- a/packages/relay/src/errors.ts +++ b/packages/relay/src/errors.ts @@ -6,6 +6,14 @@ export class UnAuthenticatedError extends Error { } } +export class FullNameRequiredError extends Error { + constructor(message?: string) { + super(message || "FULL_NAME_REQUIRED"); + this.name = "FullNameRequiredError"; + Object.setPrototypeOf(this, FullNameRequiredError.prototype); + } +} + export class NDASignatureRequiredError extends Error { constructor(message?: string) { super(message || "NDA_SIGNATURE_REQUIRED"); diff --git a/packages/relay/src/fetch.ts b/packages/relay/src/fetch.ts index 95e86b37f..fded999bd 100644 --- a/packages/relay/src/fetch.ts +++ b/packages/relay/src/fetch.ts @@ -5,12 +5,16 @@ import { ForbiddenError, AssumptionRequiredError, NDASignatureRequiredError, + FullNameRequiredError, } from "./errors"; import { GraphQLError } from "graphql"; const hasUnauthenticatedError = (error: GraphQLError) => error.extensions?.code == "UNAUTHENTICATED"; +const hasFullNameRequiredError = (error: GraphQLError) => + error.extensions?.code == "FULL_NAME_REQUIRED"; + const hasAssumptionRequiredError = (error: GraphQLError) => error.extensions?.code == "ASSUMPTION_REQUIRED"; @@ -83,6 +87,11 @@ export const makeFetchQuery = (endpoint: string): FetchFunction => { throw new UnAuthenticatedError(unauthenticatedError.message); } + const fullNameRequiredError = errors.find(hasFullNameRequiredError); + if (fullNameRequiredError) { + throw new FullNameRequiredError(fullNameRequiredError.message); + } + const assumptionRequiredError = errors.find(hasAssumptionRequiredError); if (assumptionRequiredError) { throw new AssumptionRequiredError(assumptionRequiredError.message) diff --git a/pkg/iam/account_service.go b/pkg/iam/account_service.go index 224508684..489c9d647 100644 --- a/pkg/iam/account_service.go +++ b/pkg/iam/account_service.go @@ -47,6 +47,15 @@ type ( IdentityID gid.GID `json:"uid"` Email mail.Addr `json:"email"` } + + ChangeEmailRequest struct { + NewEmail mail.Addr + Password string + } + + UpdateIdentityRequest struct { + FullName string `json:"fullName"` + } ) const ( @@ -57,11 +66,6 @@ func NewAccountService(svc *Service) *AccountService { return &AccountService{Service: svc} } -type ChangeEmailRequest struct { - NewEmail mail.Addr - Password string -} - func (req ChangeEmailRequest) Validate() error { v := validator.New() @@ -70,6 +74,14 @@ func (req ChangeEmailRequest) Validate() error { return v.Error() } +func (req UpdateIdentityRequest) Validate() error { + v := validator.New() + + v.Check(req.FullName, "full_name", validator.NotEmpty(), validator.MinLen(2), validator.MaxLen(255)) + + return v.Error() +} + func (s AccountService) ChangeEmail(ctx context.Context, identityID gid.GID, req *ChangeEmailRequest) error { if err := req.Validate(); err != nil { return fmt.Errorf("invalid request: %w", err) @@ -339,6 +351,42 @@ func (s AccountService) GetIdentity(ctx context.Context, identityID gid.GID) (*c return identity, nil } +func (s AccountService) UpdateIdentity(ctx context.Context, identityID gid.GID, req *UpdateIdentityRequest) (*coredata.Identity, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) + } + + identity := &coredata.Identity{} + + err := s.pg.WithTx( + ctx, + func(tx pg.Conn) error { + err := identity.LoadByID(ctx, tx, identityID) + if err != nil { + if err == coredata.ErrResourceNotFound { + return NewIdentityNotFoundError(identityID) + } + + return fmt.Errorf("cannot load identity: %w", err) + } + + identity.FullName = req.FullName + identity.UpdatedAt = time.Now() + + if err := identity.Update(ctx, tx); err != nil { + return fmt.Errorf("cannot update identity: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return identity, nil +} + func (s AccountService) ListPersonalAPIKeys( ctx context.Context, identityID gid.GID, diff --git a/pkg/iam/auth_service.go b/pkg/iam/auth_service.go index 98c403764..95605b5be 100644 --- a/pkg/iam/auth_service.go +++ b/pkg/iam/auth_service.go @@ -61,7 +61,6 @@ type ( } SendMagicLinkRequest struct { - FullName string Email mail.Addr URLPath string OrganizationID gid.GID @@ -75,7 +74,6 @@ type ( } MagicLinkData struct { - FullName string `json:"fullName"` Email mail.Addr `json:"email"` Continue *string `json:"continue"` } @@ -552,7 +550,6 @@ func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkReques TokenTypeMagicLink, s.magicLinkTokenValidity, MagicLinkData{ - FullName: req.FullName, Email: req.Email, Continue: req.Continue, }, @@ -574,9 +571,20 @@ func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkReques return fmt.Errorf("cannot insert token: %w", err) } - fullName := req.FullName + fullName := req.Email.Username() + identity := &coredata.Identity{} organization := &coredata.Organization{} + if err := identity.LoadByEmail(ctx, tx, req.Email); err == nil { + if identity.FullName != "" { + fullName = identity.FullName + } + } else { + if !errors.Is(err, coredata.ErrResourceNotFound) { + return fmt.Errorf("cannot load identity: %w", err) + } + } + if err := organization.LoadByID(ctx, tx, coredata.NewNoScope(), req.OrganizationID); err != nil { return fmt.Errorf("cannot load organization: %w", err) } @@ -675,10 +683,6 @@ func (s AuthService) OpenSessionWithMagicLink(ctx context.Context, tokenString s UpdatedAt: now, } - if identity.FullName == "" { - identity.FullName = payload.Data.FullName - } - if err := identity.Insert(ctx, tx); err != nil { return fmt.Errorf("cannot create identity: %w", err) } diff --git a/pkg/server/api/trust/v1/nda_directive.go b/pkg/server/api/trust/v1/nda_directive.go index ecd57aa04..aa20b48f2 100644 --- a/pkg/server/api/trust/v1/nda_directive.go +++ b/pkg/server/api/trust/v1/nda_directive.go @@ -65,6 +65,11 @@ func newNDADirective( return nil, gqlutils.Internal(ctx) } + // We need full name before user signs NDA + if identity.FullName == "" { + return nil, gqlutils.FullNameRequiredf(ctx, "full name is required") + } + if sig.Status != coredata.ElectronicSignatureStatusCompleted { return nil, gqlutils.NDASignatureRequiredf(ctx, "NDA signature required") } diff --git a/pkg/server/api/trust/v1/schema.graphql b/pkg/server/api/trust/v1/schema.graphql index 94ba9404c..64222612f 100644 --- a/pkg/server/api/trust/v1/schema.graphql +++ b/pkg/server/api/trust/v1/schema.graphql @@ -611,7 +611,6 @@ type DocumentAccess implements Node { } input SendMagicLinkInput { - fullName: String! email: EmailAddr! continue: String } @@ -628,6 +627,14 @@ type VerifyMagicLinkPayload { continue: String } +input UpdateFullNameInput { + fullName: String! +} + +type UpdateFullNamePayload { + success: Boolean! +} + type RequestDocumentAccessPayload { document: Document } @@ -827,6 +834,8 @@ type Mutation { @session(required: OPTIONAL) verifyMagicLink(input: VerifyMagicLinkInput!): VerifyMagicLinkPayload @session(required: OPTIONAL) + updateFullName(input: UpdateFullNameInput!): UpdateFullNamePayload + @session(required: PRESENT) requestAllAccesses: RequestAccessesPayload! @session(required: PRESENT) @nda diff --git a/pkg/server/api/trust/v1/schema/schema.go b/pkg/server/api/trust/v1/schema/schema.go index 9cd3a351f..a44513d9b 100644 --- a/pkg/server/api/trust/v1/schema/schema.go +++ b/pkg/server/api/trust/v1/schema/schema.go @@ -166,6 +166,7 @@ type ComplexityRoot struct { RequestReportAccess func(childComplexity int, input types.RequestReportAccessInput) int RequestTrustCenterFileAccess func(childComplexity int, input types.RequestTrustCenterFileAccessInput) int SendMagicLink func(childComplexity int, input types.SendMagicLinkInput) int + UpdateFullName func(childComplexity int, input types.UpdateFullNameInput) int VerifyMagicLink func(childComplexity int, input types.VerifyMagicLinkInput) int } @@ -289,6 +290,10 @@ type ComplexityRoot struct { Node func(childComplexity int) int } + UpdateFullNamePayload struct { + Success func(childComplexity int) int + } + Vendor struct { Category func(childComplexity int) int Countries func(childComplexity int) int @@ -333,6 +338,7 @@ type FrameworkResolver interface { type MutationResolver interface { SendMagicLink(ctx context.Context, input types.SendMagicLinkInput) (*types.SendMagicLinkPayload, error) VerifyMagicLink(ctx context.Context, input types.VerifyMagicLinkInput) (*types.VerifyMagicLinkPayload, error) + UpdateFullName(ctx context.Context, input types.UpdateFullNameInput) (*types.UpdateFullNamePayload, error) RequestAllAccesses(ctx context.Context) (*types.RequestAccessesPayload, error) ExportDocumentPDF(ctx context.Context, input types.ExportDocumentPDFInput) (*types.ExportDocumentPDFPayload, error) ExportReportPDF(ctx context.Context, input types.ExportReportPDFInput) (*types.ExportReportPDFPayload, error) @@ -800,6 +806,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.ComplexityRoot.Mutation.SendMagicLink(childComplexity, args["input"].(types.SendMagicLinkInput)), true + case "Mutation.updateFullName": + if e.ComplexityRoot.Mutation.UpdateFullName == nil { + break + } + + args, err := ec.field_Mutation_updateFullName_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.ComplexityRoot.Mutation.UpdateFullName(childComplexity, args["input"].(types.UpdateFullNameInput)), true case "Mutation.verifyMagicLink": if e.ComplexityRoot.Mutation.VerifyMagicLink == nil { break @@ -1245,6 +1262,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.TrustCenterReferenceEdge.Node(childComplexity), true + case "UpdateFullNamePayload.success": + if e.ComplexityRoot.UpdateFullNamePayload.Success == nil { + break + } + + return e.ComplexityRoot.UpdateFullNamePayload.Success(childComplexity), true + case "Vendor.category": if e.ComplexityRoot.Vendor.Category == nil { break @@ -1344,6 +1368,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputRequestReportAccessInput, ec.unmarshalInputRequestTrustCenterFileAccessInput, ec.unmarshalInputSendMagicLinkInput, + ec.unmarshalInputUpdateFullNameInput, ec.unmarshalInputVerifyMagicLinkInput, ) first := true @@ -2033,7 +2058,6 @@ type DocumentAccess implements Node { } input SendMagicLinkInput { - fullName: String! email: EmailAddr! continue: String } @@ -2050,6 +2074,14 @@ type VerifyMagicLinkPayload { continue: String } +input UpdateFullNameInput { + fullName: String! +} + +type UpdateFullNamePayload { + success: Boolean! +} + type RequestDocumentAccessPayload { document: Document } @@ -2249,6 +2281,8 @@ type Mutation { @session(required: OPTIONAL) verifyMagicLink(input: VerifyMagicLinkInput!): VerifyMagicLinkPayload @session(required: OPTIONAL) + updateFullName(input: UpdateFullNameInput!): UpdateFullNamePayload + @session(required: PRESENT) requestAllAccesses: RequestAccessesPayload! @session(required: PRESENT) @nda @@ -2439,6 +2473,17 @@ func (ec *executionContext) field_Mutation_sendMagicLink_args(ctx context.Contex return args, nil } +func (ec *executionContext) field_Mutation_updateFullName_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNUpdateFullNameInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐUpdateFullNameInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_verifyMagicLink_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -4390,6 +4435,69 @@ func (ec *executionContext) fieldContext_Mutation_verifyMagicLink(ctx context.Co return fc, nil } +func (ec *executionContext) _Mutation_updateFullName(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_updateFullName, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.Resolvers.Mutation().UpdateFullName(ctx, fc.Args["input"].(types.UpdateFullNameInput)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "PRESENT") + if err != nil { + var zeroVal *types.UpdateFullNamePayload + return zeroVal, err + } + if ec.Directives.Session == nil { + var zeroVal *types.UpdateFullNamePayload + return zeroVal, errors.New("directive session is not implemented") + } + return ec.Directives.Session(ctx, nil, directive0, required) + } + + next = directive1 + return next + }, + ec.marshalOUpdateFullNamePayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐUpdateFullNamePayload, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_Mutation_updateFullName(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "success": + return ec.fieldContext_UpdateFullNamePayload_success(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type UpdateFullNamePayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_updateFullName_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_requestAllAccesses(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -7405,6 +7513,35 @@ func (ec *executionContext) fieldContext_TrustCenterReferenceEdge_node(_ context return fc, nil } +func (ec *executionContext) _UpdateFullNamePayload_success(ctx context.Context, field graphql.CollectedField, obj *types.UpdateFullNamePayload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_UpdateFullNamePayload_success, + func(ctx context.Context) (any, error) { + return obj.Success, nil + }, + nil, + ec.marshalNBoolean2bool, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_UpdateFullNamePayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "UpdateFullNamePayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Vendor_id(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -9508,20 +9645,13 @@ func (ec *executionContext) unmarshalInputSendMagicLinkInput(ctx context.Context asMap[k] = v } - fieldsInOrder := [...]string{"fullName", "email", "continue"} + fieldsInOrder := [...]string{"email", "continue"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { continue } switch k { - case "fullName": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.FullName = data case "email": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) @@ -9541,6 +9671,32 @@ func (ec *executionContext) unmarshalInputSendMagicLinkInput(ctx context.Context return it, nil } +func (ec *executionContext) unmarshalInputUpdateFullNameInput(ctx context.Context, obj any) (types.UpdateFullNameInput, error) { + var it types.UpdateFullNameInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"fullName"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "fullName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.FullName = data + } + } + return it, nil +} + func (ec *executionContext) unmarshalInputVerifyMagicLinkInput(ctx context.Context, obj any) (types.VerifyMagicLinkInput, error) { var it types.VerifyMagicLinkInput asMap := map[string]any{} @@ -10722,6 +10878,10 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_verifyMagicLink(ctx, field) }) + case "updateFullName": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateFullName(ctx, field) + }) case "requestAllAccesses": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_requestAllAccesses(ctx, field) @@ -12331,6 +12491,45 @@ func (ec *executionContext) _TrustCenterReferenceEdge(ctx context.Context, sel a return out } +var updateFullNamePayloadImplementors = []string{"UpdateFullNamePayload"} + +func (ec *executionContext) _UpdateFullNamePayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateFullNamePayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, updateFullNamePayloadImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("UpdateFullNamePayload") + case "success": + out.Values[i] = ec._UpdateFullNamePayload_success(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.Deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.ProcessDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var vendorImplementors = []string{"Vendor", "Node"} func (ec *executionContext) _Vendor(ctx context.Context, sel ast.SelectionSet, obj *types.Vendor) graphql.Marshaler { @@ -14226,6 +14425,11 @@ func (ec *executionContext) marshalNTrustCenterReferenceEdge2ᚖgoᚗproboᚗinc return ec._TrustCenterReferenceEdge(ctx, sel, v) } +func (ec *executionContext) unmarshalNUpdateFullNameInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐUpdateFullNameInput(ctx context.Context, v any) (types.UpdateFullNameInput, error) { + res, err := ec.unmarshalInputUpdateFullNameInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNVendor2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐVendor(ctx context.Context, sel ast.SelectionSet, v *types.Vendor) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -14712,6 +14916,13 @@ func (ec *executionContext) marshalOTrustCenterFile2ᚖgoᚗproboᚗincᚋprobo return ec._TrustCenterFile(ctx, sel, v) } +func (ec *executionContext) marshalOUpdateFullNamePayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐUpdateFullNamePayload(ctx context.Context, sel ast.SelectionSet, v *types.UpdateFullNamePayload) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._UpdateFullNamePayload(ctx, sel, v) +} + func (ec *executionContext) marshalOVerifyMagicLinkPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐVerifyMagicLinkPayload(ctx context.Context, sel ast.SelectionSet, v *types.VerifyMagicLinkPayload) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/server/api/trust/v1/types/types.go b/pkg/server/api/trust/v1/types/types.go index b997a084d..beec30cef 100644 --- a/pkg/server/api/trust/v1/types/types.go +++ b/pkg/server/api/trust/v1/types/types.go @@ -213,7 +213,6 @@ type RequestTrustCenterFileAccessInput struct { } type SendMagicLinkInput struct { - FullName string `json:"fullName"` Email mail.Addr `json:"email"` Continue *string `json:"continue,omitempty"` } @@ -294,6 +293,14 @@ type TrustCenterReferenceEdge struct { Node *TrustCenterReference `json:"node"` } +type UpdateFullNameInput struct { + FullName string `json:"fullName"` +} + +type UpdateFullNamePayload struct { + Success bool `json:"success"` +} + type Vendor struct { ID gid.GID `json:"id"` Name string `json:"name"` diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go index 943a2d564..d75d58dba 100644 --- a/pkg/server/api/trust/v1/v1_resolver.go +++ b/pkg/server/api/trust/v1/v1_resolver.go @@ -192,7 +192,6 @@ func (r *mutationResolver) SendMagicLink(ctx context.Context, input types.SendMa } req := &iam.SendMagicLinkRequest{ - FullName: input.FullName, Email: input.Email, CompliancePageID: &trustCenter.ID, OrganizationID: trustCenter.OrganizationID, @@ -273,6 +272,49 @@ func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.Veri }, 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 { + if _, ok := errors.AsType[*iam.ErrProfileNotFound](err); !ok { + 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 +} + // RequestAllAccesses is the resolver for the requestAllAccesses field. func (r *mutationResolver) RequestAllAccesses(ctx context.Context) (*types.RequestAccessesPayload, error) { trustCenter := compliancepage.CompliancePageFromContext(ctx) diff --git a/pkg/server/gqlutils/errors.go b/pkg/server/gqlutils/errors.go index 5dce8027c..4e2b3a9eb 100644 --- a/pkg/server/gqlutils/errors.go +++ b/pkg/server/gqlutils/errors.go @@ -66,6 +66,20 @@ func AssumptionRequiredf(ctx context.Context, format string, a ...any) *gqlerror return AssumptionRequired(ctx, fmt.Errorf(format, a...)) } +func FullNameRequired(ctx context.Context, err error) *gqlerror.Error { + return &gqlerror.Error{ + Message: err.Error(), + Path: graphql.GetPath(ctx), + Extensions: map[string]any{ + "code": "FULL_NAME_REQUIRED", + }, + } +} + +func FullNameRequiredf(ctx context.Context, format string, a ...any) *gqlerror.Error { + return FullNameRequired(ctx, fmt.Errorf(format, a...)) +} + func NDASignatureRequired(ctx context.Context, err error) *gqlerror.Error { return &gqlerror.Error{ Message: err.Error(), diff --git a/pkg/trust/trust_center_access_service.go b/pkg/trust/trust_center_access_service.go index f1e3162da..ec8a1f8a9 100644 --- a/pkg/trust/trust_center_access_service.go +++ b/pkg/trust/trust_center_access_service.go @@ -187,7 +187,7 @@ func (s TrustCenterAccessService) Request( return fmt.Errorf("cannot load trust center: %w", err) } - access := &coredata.TrustCenterAccess{} + access = &coredata.TrustCenterAccess{} if err := access.LoadByTrustCenterIDAndIdentityID(ctx, tx, s.svc.scope, req.TrustCenterID, req.IdentityID); err != nil { return fmt.Errorf("cannot load compliance page membership: %w", err) }