From 76f6c8f9d1a3b942c38b4d836e78c12bb2e93c27 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Wed, 8 Apr 2026 18:29:36 +0200 Subject: [PATCH] Show registration unavailable page when signup is disabled Instead of showing the signup form and returning an internal error on submit, the SignUpPage now queries signUpEnabled upfront and displays a friendly message explaining that registration is not available, with a link back to login. Adds a signUpEnabled GraphQL query field on the connect/v1 API and handles ErrSignupDisabled as a FORBIDDEN error in the SignUp resolver. Signed-off-by: Sacha Al Himdani --- .../console/src/pages/iam/auth/SignUpPage.tsx | 49 ++++++++++++++++++- pkg/iam/service.go | 4 ++ pkg/server/api/connect/v1/schema.graphql | 3 ++ pkg/server/api/connect/v1/v1_resolver.go | 10 ++++ 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/apps/console/src/pages/iam/auth/SignUpPage.tsx b/apps/console/src/pages/iam/auth/SignUpPage.tsx index 0ed750772..c8f2cbd8d 100644 --- a/apps/console/src/pages/iam/auth/SignUpPage.tsx +++ b/apps/console/src/pages/iam/auth/SignUpPage.tsx @@ -16,14 +16,22 @@ import { formatError } from "@probo/helpers"; import { usePageTitle } from "@probo/hooks"; import { useTranslate } from "@probo/i18n"; import { Button, Field, useToast } from "@probo/ui"; -import { useMutation } from "react-relay"; +import { useEffect } from "react"; +import { useMutation, usePreloadedQuery, useQueryLoader } from "react-relay"; import { Link, useNavigate } from "react-router"; import { graphql } from "relay-runtime"; import { z } from "zod"; import type { SignUpPageMutation } from "#/__generated__/iam/SignUpPageMutation.graphql"; +import type { SignUpPageQuery } from "#/__generated__/iam/SignUpPageQuery.graphql"; import { useFormWithSchema } from "#/hooks/useFormWithSchema"; +const signUpPageQuery = graphql` + query SignUpPageQuery { + signUpEnabled + } +`; + const signUpMutation = graphql` mutation SignUpPageMutation($input: SignUpInput!) { signUp(input: $input) { @@ -42,13 +50,15 @@ const schema = z.object({ type FormData = z.infer; -export default function SignUpPage() { +function SignUpPageContent(props: { queryRef: NonNullable>[0]> }) { const { __ } = useTranslate(); const { toast } = useToast(); const navigate = useNavigate(); usePageTitle(__("Sign up")); + const data = usePreloadedQuery(signUpPageQuery, props.queryRef); + const { register, handleSubmit, formState } = useFormWithSchema(schema, { defaultValues: { email: "", @@ -95,6 +105,29 @@ export default function SignUpPage() { }); }; + if (!data.signUpEnabled) { + return ( +
+
+

{__("Registration unavailable")}

+

+ {__("New account registration is currently disabled. Please contact your administrator or reach out to Probo for assistance.")} +

+
+ +
+ +
+
+ ); + } + return (
@@ -154,3 +187,15 @@ export default function SignUpPage() {
); } + +export default function SignUpPage() { + const [queryRef, loadQuery] = useQueryLoader(signUpPageQuery); + + useEffect(() => { + loadQuery({}); + }, [loadQuery]); + + if (!queryRef) return null; + + return ; +} diff --git a/pkg/iam/service.go b/pkg/iam/service.go index 89547b42b..dd50f2fe8 100644 --- a/pkg/iam/service.go +++ b/pkg/iam/service.go @@ -188,6 +188,10 @@ func NewService( return svc, nil } +func (s *Service) IsSignUpEnabled() bool { + return !s.disableSignup +} + func (s *Service) Run(ctx context.Context) error { wg := sync.WaitGroup{} ctx, cancel := context.WithCancelCause(ctx) diff --git a/pkg/server/api/connect/v1/schema.graphql b/pkg/server/api/connect/v1/schema.graphql index a97554c83..b0b9337ad 100644 --- a/pkg/server/api/connect/v1/schema.graphql +++ b/pkg/server/api/connect/v1/schema.graphql @@ -55,6 +55,9 @@ type Query { oidcProviders: [OIDCProviderInfo!]! @goField(forceResolver: true) @session(required: OPTIONAL) + signUpEnabled: Boolean! + @goField(forceResolver: true) + @session(required: NONE) } type Mutation { diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index abfff6775..d49642043 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -346,6 +346,11 @@ func (r *mutationResolver) SignUp(ctx context.Context, input types.SignUpInput) return nil, gqlutils.Invalid(ctx, err) } + var errSignupDisabled *iam.ErrSignupDisabled + if errors.As(err, &errSignupDisabled) { + return nil, gqlutils.Forbidden(ctx, err) + } + r.logger.ErrorCtx(ctx, "cannot create identity with password", log.Error(err)) return nil, gqlutils.Internal(ctx) } @@ -1777,6 +1782,11 @@ func (r *queryResolver) OidcProviders(ctx context.Context) ([]*types.OIDCProvide return result, nil } +// SignUpEnabled is the resolver for the signUpEnabled field. +func (r *queryResolver) SignUpEnabled(ctx context.Context) (bool, error) { + return r.iam.IsSignUpEnabled(), nil +} + // TestLoginURL is the resolver for the testLoginUrl field. func (r *sAMLConfigurationResolver) TestLoginURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error) { return r.baseURL.WithPath("/api/connect/v1/saml/2.0/" + obj.ID.String()).MustString(), nil