Make self-service signup opt-in via SIGNUP_ENABLED
/users/signup answers 403 unless SIGNUP_ENABLED is set. A public GET /utils/config/ tells the login page, which hides the sign-up link, and the signup page explains that an administrator creates accounts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013GAeg4Jo6xjZzjwy9QDWMQ
This commit is contained in:
@@ -2961,6 +2961,19 @@ export const PrivateUserCreateSchema = {
|
||||
title: 'PrivateUserCreate'
|
||||
} as const;
|
||||
|
||||
export const PublicConfigSchema = {
|
||||
properties: {
|
||||
signup_enabled: {
|
||||
type: 'boolean',
|
||||
title: 'Signup Enabled'
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['signup_enabled'],
|
||||
title: 'PublicConfig',
|
||||
description: 'What a browser needs to know about this instance before signing in.'
|
||||
} as const;
|
||||
|
||||
export const RegisteredClientSchema = {
|
||||
properties: {
|
||||
id: {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1006,6 +1006,13 @@ export type PrivateUserCreate = {
|
||||
is_verified?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* What a browser needs to know about this instance before signing in.
|
||||
*/
|
||||
export type PublicConfig = {
|
||||
signup_enabled: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A registered agent, and whether anyone actually let it in.
|
||||
*/
|
||||
@@ -2156,6 +2163,8 @@ export type UtilsTestEmailData = {
|
||||
|
||||
export type UtilsTestEmailResponse = (Message);
|
||||
|
||||
export type UtilsReadConfigResponse = (PublicConfig);
|
||||
|
||||
export type UtilsHealthCheckResponse = (boolean);
|
||||
|
||||
export type UtilsHealthResponse = ({
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type UserPublic,
|
||||
type UserRegister,
|
||||
UsersService,
|
||||
UtilsService,
|
||||
} from "@/client"
|
||||
import { liveStore } from "@/components/Flow/liveStore"
|
||||
import { isPortal } from "@/lib/portal"
|
||||
@@ -32,6 +33,14 @@ export function safeRedirect(target: string | undefined): string {
|
||||
return target
|
||||
}
|
||||
|
||||
/** Whether this instance lets a visitor create an account; unknown while loading. */
|
||||
export const useSignupEnabled = (): boolean | undefined =>
|
||||
useQuery({
|
||||
queryKey: ["config"],
|
||||
queryFn: UtilsService.readConfig,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
}).data?.signup_enabled
|
||||
|
||||
const useAuth = () => {
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -20,7 +20,11 @@ import {
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { LoadingButton } from "@/components/ui/loading-button"
|
||||
import { PasswordInput } from "@/components/ui/password-input"
|
||||
import useAuth, { isLoggedIn, safeRedirect } from "@/hooks/useAuth"
|
||||
import useAuth, {
|
||||
isLoggedIn,
|
||||
safeRedirect,
|
||||
useSignupEnabled,
|
||||
} from "@/hooks/useAuth"
|
||||
|
||||
const formSchema = z.object({
|
||||
username: z.email(),
|
||||
@@ -55,6 +59,7 @@ export const Route = createFileRoute("/login")({
|
||||
|
||||
function Login() {
|
||||
const { loginMutation } = useAuth()
|
||||
const signupEnabled = useSignupEnabled()
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
mode: "onBlur",
|
||||
@@ -132,12 +137,14 @@ function Login() {
|
||||
</LoadingButton>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm">
|
||||
Don't have an account yet?{" "}
|
||||
<RouterLink to="/signup" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</RouterLink>
|
||||
</div>
|
||||
{signupEnabled ? (
|
||||
<div className="text-center text-sm">
|
||||
Don't have an account yet?{" "}
|
||||
<RouterLink to="/signup" className="underline underline-offset-4">
|
||||
Sign up
|
||||
</RouterLink>
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
</Form>
|
||||
</AuthLayout>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { LoadingButton } from "@/components/ui/loading-button"
|
||||
import { PasswordInput } from "@/components/ui/password-input"
|
||||
import useAuth, { isLoggedIn } from "@/hooks/useAuth"
|
||||
import useAuth, { isLoggedIn, useSignupEnabled } from "@/hooks/useAuth"
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
@@ -59,6 +59,7 @@ export const Route = createFileRoute("/signup")({
|
||||
|
||||
function SignUp() {
|
||||
const { signUpMutation } = useAuth()
|
||||
const signupEnabled = useSignupEnabled()
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
mode: "onBlur",
|
||||
@@ -79,6 +80,30 @@ function SignUp() {
|
||||
signUpMutation.mutate(submitData)
|
||||
}
|
||||
|
||||
if (signupEnabled === false) {
|
||||
return (
|
||||
<AuthLayout>
|
||||
<div
|
||||
className="flex flex-col gap-6 text-center"
|
||||
data-testid="signup-disabled"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<h1 className="text-2xl font-bold">Sign up is off</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Accounts on this instance are created by an administrator.
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
Already have an account?{" "}
|
||||
<RouterLink to="/login" className="underline underline-offset-4">
|
||||
Log in
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</AuthLayout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<Form {...form}>
|
||||
|
||||
Reference in New Issue
Block a user