Default signups off, improve Docker deploy, fix onboarding step UI.

Show a disabled-registration state on the register page when DISABLE_SIGNUPS is true (default), document docker-deploy.sh with git-SHA image tags, and align onboarding progress circles and labels on a shared grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 13:14:25 -04:00
co-authored by Cursor
parent 6b73c32c25
commit 85df7c4627
13 changed files with 163 additions and 70 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
import { env } from "~/env";
import { RegisterForm } from "./register-form";
export default function RegisterPage() {
return <RegisterForm />;
return <RegisterForm signupsDisabled={env.DISABLE_SIGNUPS === true} />;
}
+35 -2
View File
@@ -3,7 +3,7 @@
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { ArrowRight, Lock, Mail, User } from "lucide-react";
import { ArrowRight, Lock, Mail, User, UserX } from "lucide-react";
import {
AuthCard,
AuthCardHeader,
@@ -22,7 +22,11 @@ function formatAuthError(message: string | undefined, fallback: string): string
return message;
}
export function RegisterForm() {
interface RegisterFormProps {
signupsDisabled?: boolean;
}
export function RegisterForm({ signupsDisabled = false }: RegisterFormProps) {
const router = useRouter();
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
@@ -92,6 +96,35 @@ export function RegisterForm() {
}
}
if (signupsDisabled) {
return (
<AuthPageShell>
<AuthCard>
<AuthCardHeader
title="Registration closed"
description="New account sign-ups are not available right now"
/>
<div className="bg-muted/50 text-muted-foreground mb-6 flex gap-3 rounded-xl border px-4 py-3 text-sm">
<UserX className="text-muted-foreground mt-0.5 h-4 w-4 shrink-0" />
<p>
This workspace is not accepting new registrations. If you already
have an account, sign in below. Contact your administrator if you
need access.
</p>
</div>
<Button asChild className="h-11 w-full">
<Link href="/auth/signin">
Sign in to your account
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</AuthCard>
</AuthPageShell>
);
}
return (
<AuthPageShell>
<AuthCard>
+2 -3
View File
@@ -26,7 +26,6 @@ export function SignInForm({ allowRegistration }: SignInFormProps) {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
const signupDisabled = searchParams.get("signup") === "disabled";
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
@@ -74,7 +73,7 @@ export function SignInForm({ allowRegistration }: SignInFormProps) {
description="Sign in to your workspace"
/>
{signupDisabled && (
{!allowRegistration && (
<p className="bg-muted/50 text-muted-foreground mb-5 rounded-xl border px-3 py-2.5 text-sm">
New account registration is currently disabled.
</p>
@@ -155,7 +154,7 @@ export function SignInForm({ allowRegistration }: SignInFormProps) {
</Button>
</form>
{allowRegistration && !signupDisabled && (
{allowRegistration && (
<p className="text-muted-foreground mt-6 text-center text-sm">
Don&apos;t have an account?{" "}
<Link
@@ -14,72 +14,86 @@ function stepIndex(step: OnboardingStepId) {
return ONBOARDING_STEPS.findIndex((item) => item.id === step);
}
const TRACK_GRID_COLUMNS = ONBOARDING_STEPS.map((_, index) =>
index < ONBOARDING_STEPS.length - 1 ? "auto 1fr" : "auto",
).join(" ");
export function OnboardingStepIndicator({ step }: { step: OnboardingStepId }) {
const currentIndex = stepIndex(step);
return (
<nav aria-label="Setup progress" className="mb-8">
<ol className="mx-auto flex w-full max-w-md">
<ol className="sr-only">
{ONBOARDING_STEPS.map((item, index) => {
const isCurrent = currentIndex === index;
return (
<li key={item.id} aria-current={isCurrent ? "step" : undefined}>
{item.label}
{isCurrent ? " (current)" : ""}
</li>
);
})}
</ol>
{/* Row 1: circles + connectors. Row 2: labels (same columns as circles). */}
<div
className="mx-auto grid w-full max-w-md items-center gap-y-2"
style={{
gridTemplateColumns: TRACK_GRID_COLUMNS,
gridTemplateRows: "auto auto",
}}
aria-hidden
>
{ONBOARDING_STEPS.map((item, index) => {
const isComplete = currentIndex > index;
const isCurrent = currentIndex === index;
const isUpcoming = currentIndex < index;
const connectorComplete = currentIndex > index;
const circleCol = index * 2 + 1;
return (
<li key={item.id} className="flex flex-1 flex-col items-center">
<div className="flex w-full items-center">
{index > 0 && (
<div
className={cn(
"h-0.5 flex-1 rounded-full transition-colors",
connectorComplete || isCurrent
? "bg-primary"
: "bg-border/80",
)}
aria-hidden
/>
)}
<div key={item.id} className="contents">
{index > 0 && (
<div
className={cn(
"flex h-9 w-9 shrink-0 items-center justify-center rounded-full border-2 text-sm font-medium transition-colors",
isComplete &&
"border-primary bg-primary text-primary-foreground",
isCurrent &&
"border-primary bg-primary/10 text-primary ring-primary/20 ring-4",
isUpcoming &&
"border-border/80 bg-background/60 text-muted-foreground",
"h-0.5 self-center rounded-full transition-colors",
connectorComplete ? "bg-primary" : "bg-border/80",
)}
aria-current={isCurrent ? "step" : undefined}
>
{isComplete ? (
<Check className="h-4 w-4" aria-hidden />
) : (
<span>{index + 1}</span>
)}
</div>
{index < ONBOARDING_STEPS.length - 1 && (
<div
className={cn(
"h-0.5 flex-1 rounded-full transition-colors",
connectorComplete ? "bg-primary" : "bg-border/80",
)}
aria-hidden
/>
style={{ gridColumn: index * 2, gridRow: 1 }}
/>
)}
<div
className={cn(
"flex h-9 w-9 items-center justify-center justify-self-center rounded-full border-2 text-sm font-medium transition-colors",
isComplete &&
"border-primary bg-primary text-primary-foreground",
isCurrent &&
"border-primary bg-primary/10 text-primary ring-primary/20 ring-4",
isUpcoming &&
"border-border/80 bg-background/60 text-muted-foreground",
)}
style={{ gridColumn: circleCol, gridRow: 1 }}
>
{isComplete ? (
<Check className="h-4 w-4" aria-hidden />
) : (
<span>{index + 1}</span>
)}
</div>
<span
className={cn(
"mt-2 hidden text-xs font-medium sm:block",
"hidden min-w-0 justify-self-center text-center text-xs leading-tight font-medium sm:block",
isCurrent ? "text-foreground" : "text-muted-foreground",
)}
style={{ gridColumn: circleCol, gridRow: 2 }}
>
{item.label}
</span>
</li>
</div>
);
})}
</ol>
</div>
<p className="text-muted-foreground mt-4 text-center text-sm sm:hidden">
Step {Math.min(currentIndex + 1, ONBOARDING_STEPS.length)} of{" "}
{ONBOARDING_STEPS.length}
+1 -1
View File
@@ -33,7 +33,7 @@ export const env = createEnv({
.enum(["development", "test", "production"])
.default("development"),
DB_DISABLE_SSL: optionalEnvBoolean(),
DISABLE_SIGNUPS: optionalEnvBoolean(),
DISABLE_SIGNUPS: optionalEnvBoolean().default(true),
CRON_SECRET: z.string().optional(),
// SSO / Authentik (optional)
AUTHENTIK_ISSUER: z.string().url().optional(),
+2 -2
View File
@@ -3,7 +3,7 @@ import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { nextCookies } from "better-auth/next-js";
import { genericOAuth } from "better-auth/plugins";
import { envBoolean } from "~/lib/env-boolean";
import { env } from "~/env";
import { isDemoUser, promoteFirstRealUserIfNeeded } from "~/lib/first-admin";
import { db } from "~/server/db";
import * as schema from "~/server/db/schema";
@@ -13,7 +13,7 @@ const authentikEnabled = Boolean(
process.env.AUTHENTIK_CLIENT_ID &&
process.env.AUTHENTIK_CLIENT_SECRET,
);
const signupsDisabled = envBoolean(process.env.DISABLE_SIGNUPS);
const signupsDisabled = env.DISABLE_SIGNUPS;
// Derive the authentik origin from the issuer URL so the OAuth callback is
// automatically trusted without needing a separate AUTHENTIK_ORIGIN env var.
-7
View File
@@ -1,17 +1,10 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { envBoolean } from "~/lib/env-boolean";
import { isPublicRoute } from "~/lib/public-routes";
export function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname === "/auth/register" && envBoolean(process.env.DISABLE_SIGNUPS)) {
const signInUrl = new URL("/auth/signin", request.url);
signInUrl.searchParams.set("signup", "disabled");
return NextResponse.redirect(signInUrl);
}
// Define API routes that should be handled separately
const apiRoutes = ["/api/auth", "/api/trpc", "/api/mcp", "/api/i"];