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:
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user