feat(auth): permite bloquear novos cadastros

This commit is contained in:
Felipe Coutinho
2026-05-21 13:46:26 +00:00
parent 3a768bc8ba
commit 21d7396c80
9 changed files with 86 additions and 26 deletions

View File

@@ -17,6 +17,8 @@ POSTGRES_DB=openmonetis_db
# Gere com: openssl rand -base64 32 # Gere com: openssl rand -base64 32
BETTER_AUTH_SECRET=your-secret-key-here-change-this BETTER_AUTH_SECRET=your-secret-key-here-change-this
BETTER_AUTH_URL=http://localhost:3000 BETTER_AUTH_URL=http://localhost:3000
# Defina como true para bloquear novos cadastros
DISABLE_SIGNUP=false
# === Portas === # === Portas ===
APP_PORT=3000 APP_PORT=3000

View File

@@ -1,5 +1,6 @@
import { LoginForm } from "@/features/auth/components/login-form"; import { LoginForm } from "@/features/auth/components/login-form";
import { isSignupDisabled } from "@/shared/lib/auth/signup";
export default function LoginPage() { export default function LoginPage() {
return <LoginForm />; return <LoginForm signupDisabled={isSignupDisabled()} />;
} }

View File

@@ -1,5 +1,11 @@
import { redirect } from "next/navigation";
import { SignupForm } from "@/features/auth/components/signup-form"; import { SignupForm } from "@/features/auth/components/signup-form";
import { isSignupDisabled } from "@/shared/lib/auth/signup";
export default function SignupPage() { export default function SignupPage() {
if (isSignupDisabled()) {
redirect("/login");
}
return <SignupForm />; return <SignupForm />;
} }

View File

@@ -30,6 +30,7 @@ import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
import { Card, CardContent } from "@/shared/components/ui/card"; import { Card, CardContent } from "@/shared/components/ui/card";
import { getOptionalUserSession } from "@/shared/lib/auth/server"; import { getOptionalUserSession } from "@/shared/lib/auth/server";
import { isSignupDisabled } from "@/shared/lib/auth/signup";
export default async function Page() { export default async function Page() {
const [session, headersList, githubStats] = await Promise.all([ const [session, headersList, githubStats] = await Promise.all([
@@ -43,6 +44,7 @@ export default async function Page() {
"", "",
).replace(/:\d+$/, ""); ).replace(/:\d+$/, "");
const isPublicDomain = !!(publicDomain && hostname === publicDomain); const isPublicDomain = !!(publicDomain && hostname === publicDomain);
const signupDisabled = isSignupDisabled();
const metricsItems = getMetricsItems(githubStats.stars, githubStats.forks); const metricsItems = getMetricsItems(githubStats.stars, githubStats.forks);
return ( return (
@@ -86,6 +88,7 @@ export default async function Page() {
Entrar Entrar
</Button> </Button>
</Link> </Link>
{!signupDisabled && (
<Link href="/signup"> <Link href="/signup">
<Button <Button
variant="ghost" variant="ghost"
@@ -95,11 +98,13 @@ export default async function Page() {
Começar Começar
</Button> </Button>
</Link> </Link>
)}
</div> </div>
))} ))}
<MobileNav <MobileNav
isPublicDomain={isPublicDomain} isPublicDomain={isPublicDomain}
isLoggedIn={!!session?.user} isLoggedIn={!!session?.user}
signupDisabled={signupDisabled}
/> />
</nav> </nav>
</NavbarShell> </NavbarShell>

View File

@@ -21,10 +21,18 @@ import { GoogleAuthButton } from "./google-auth-button";
type DivProps = React.ComponentProps<"div">; type DivProps = React.ComponentProps<"div">;
interface LoginFormProps extends DivProps {
signupDisabled?: boolean;
}
const authLinkClassName = const authLinkClassName =
"font-medium text-foreground/88 underline decoration-border underline-offset-4 transition-colors hover:text-foreground hover:decoration-foreground/30 focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"; "font-medium text-foreground/88 underline decoration-border underline-offset-4 transition-colors hover:text-foreground hover:decoration-foreground/30 focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40";
export function LoginForm({ className, ...props }: DivProps) { export function LoginForm({
className,
signupDisabled = false,
...props
}: LoginFormProps) {
const router = useRouter(); const router = useRouter();
const isGoogleAvailable = googleSignInAvailable; const isGoogleAvailable = googleSignInAvailable;
@@ -233,12 +241,14 @@ export function LoginForm({ className, ...props }: DivProps) {
</div> </div>
</Field> </Field>
{!signupDisabled && (
<FieldDescription className="pt-1 text-center"> <FieldDescription className="pt-1 text-center">
Não tem uma conta?{" "} Não tem uma conta?{" "}
<a href="/signup" className={authLinkClassName}> <a href="/signup" className={authLinkClassName}>
Inscreva-se Inscreva-se
</a> </a>
</FieldDescription> </FieldDescription>
)}
<FieldDescription className="text-center text-sm text-muted-foreground"> <FieldDescription className="text-center text-sm text-muted-foreground">
<a href="/" className={authLinkClassName}> <a href="/" className={authLinkClassName}>

View File

@@ -23,9 +23,14 @@ const navLinks = [
interface MobileNavProps { interface MobileNavProps {
isPublicDomain: boolean; isPublicDomain: boolean;
isLoggedIn: boolean; isLoggedIn: boolean;
signupDisabled: boolean;
} }
export function MobileNav({ isPublicDomain, isLoggedIn }: MobileNavProps) { export function MobileNav({
isPublicDomain,
isLoggedIn,
signupDisabled,
}: MobileNavProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
@@ -75,12 +80,14 @@ export function MobileNav({ isPublicDomain, isLoggedIn }: MobileNavProps) {
Entrar Entrar
</Button> </Button>
</Link> </Link>
{!signupDisabled && (
<Link href="/signup" onClick={() => setOpen(false)}> <Link href="/signup" onClick={() => setOpen(false)}>
<Button className="w-full gap-2"> <Button className="w-full gap-2">
Começar Começar
<RiArrowRightSLine size={16} /> <RiArrowRightSLine size={16} />
</Button> </Button>
</Link> </Link>
)}
</> </>
)} )}
</div> </div>

View File

@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from "next/server"; import { type NextRequest, NextResponse } from "next/server";
import { auth } from "@/shared/lib/auth/config"; import { auth } from "@/shared/lib/auth/config";
import { isSignupDisabled } from "@/shared/lib/auth/signup";
// Rotas protegidas que requerem autenticação // Rotas protegidas que requerem autenticação
const PROTECTED_ROUTES = [ const PROTECTED_ROUTES = [
@@ -85,6 +86,22 @@ export default async function proxy(request: NextRequest) {
}); });
const isAuthenticated = !!session?.user; const isAuthenticated = !!session?.user;
const signupDisabled = isSignupDisabled();
if (signupDisabled) {
if (pathname === "/signup" || pathname.startsWith("/signup/")) {
return NextResponse.redirect(
new URL(isAuthenticated ? "/dashboard" : "/login", request.url),
);
}
if (pathname.startsWith("/api/auth/sign-up")) {
return NextResponse.json(
{ error: "Novos cadastros estão desativados." },
{ status: 403 },
);
}
}
// Redirect authenticated users away from login/signup pages // Redirect authenticated users away from login/signup pages
if (isAuthenticated && PUBLIC_AUTH_ROUTES.includes(pathname)) { if (isAuthenticated && PUBLIC_AUTH_ROUTES.includes(pathname)) {

View File

@@ -1,7 +1,8 @@
import { passkey } from "@better-auth/passkey"; import { passkey } from "@better-auth/passkey";
import { betterAuth } from "better-auth"; import { APIError, betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { drizzleAdapter } from "better-auth/adapters/drizzle";
import type { GoogleProfile } from "better-auth/social-providers"; import type { GoogleProfile } from "better-auth/social-providers";
import { isSignupDisabled } from "@/shared/lib/auth/signup";
import { seedDefaultCategoriesForUser } from "@/shared/lib/categories/defaults"; import { seedDefaultCategoriesForUser } from "@/shared/lib/categories/defaults";
import { db, schema } from "@/shared/lib/db"; import { db, schema } from "@/shared/lib/db";
import { ensureDefaultPayerForUser } from "@/shared/lib/payers/defaults"; import { ensureDefaultPayerForUser } from "@/shared/lib/payers/defaults";
@@ -122,6 +123,13 @@ export const auth = betterAuth({
databaseHooks: { databaseHooks: {
user: { user: {
create: { create: {
before: async () => {
if (!isSignupDisabled()) return;
throw new APIError("FORBIDDEN", {
message: "Novos cadastros estão desativados.",
});
},
/** /**
* Após criar novo usuário, inicializa: * Após criar novo usuário, inicializa:
* 1. Categorias padrão (Receitas/Despesas) * 1. Categorias padrão (Receitas/Despesas)

View File

@@ -0,0 +1,4 @@
export function isSignupDisabled(): boolean {
const value = process.env.DISABLE_SIGNUP?.trim().toLowerCase();
return value === "true";
}