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
BETTER_AUTH_SECRET=your-secret-key-here-change-this
BETTER_AUTH_URL=http://localhost:3000
# Defina como true para bloquear novos cadastros
DISABLE_SIGNUP=false
# === Portas ===
APP_PORT=3000
@@ -59,4 +61,4 @@ OPENROUTER_API_KEY=
# === Logo.dev (Opcional) ===
# Logos automáticos de estabelecimentos. Cadastre em https://www.logo.dev
LOGO_DEV_TOKEN=
LOGO_DEV_SECRET_KEY=
LOGO_DEV_SECRET_KEY=

View File

@@ -1,5 +1,6 @@
import { LoginForm } from "@/features/auth/components/login-form";
import { isSignupDisabled } from "@/shared/lib/auth/signup";
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 { isSignupDisabled } from "@/shared/lib/auth/signup";
export default function SignupPage() {
if (isSignupDisabled()) {
redirect("/login");
}
return <SignupForm />;
}

View File

@@ -30,6 +30,7 @@ import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button";
import { Card, CardContent } from "@/shared/components/ui/card";
import { getOptionalUserSession } from "@/shared/lib/auth/server";
import { isSignupDisabled } from "@/shared/lib/auth/signup";
export default async function Page() {
const [session, headersList, githubStats] = await Promise.all([
@@ -43,6 +44,7 @@ export default async function Page() {
"",
).replace(/:\d+$/, "");
const isPublicDomain = !!(publicDomain && hostname === publicDomain);
const signupDisabled = isSignupDisabled();
const metricsItems = getMetricsItems(githubStats.stars, githubStats.forks);
return (
@@ -86,20 +88,23 @@ export default async function Page() {
Entrar
</Button>
</Link>
<Link href="/signup">
<Button
variant="ghost"
size="sm"
className="h-9 text-primary-foreground/75 hover:bg-primary-foreground/10 hover:text-primary-foreground shadow-none dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
Começar
</Button>
</Link>
{!signupDisabled && (
<Link href="/signup">
<Button
variant="ghost"
size="sm"
className="h-9 text-primary-foreground/75 hover:bg-primary-foreground/10 hover:text-primary-foreground shadow-none dark:text-white/75 dark:hover:bg-white/10 dark:hover:text-white"
>
Começar
</Button>
</Link>
)}
</div>
))}
<MobileNav
isPublicDomain={isPublicDomain}
isLoggedIn={!!session?.user}
signupDisabled={signupDisabled}
/>
</nav>
</NavbarShell>

View File

@@ -21,10 +21,18 @@ import { GoogleAuthButton } from "./google-auth-button";
type DivProps = React.ComponentProps<"div">;
interface LoginFormProps extends DivProps {
signupDisabled?: boolean;
}
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";
export function LoginForm({ className, ...props }: DivProps) {
export function LoginForm({
className,
signupDisabled = false,
...props
}: LoginFormProps) {
const router = useRouter();
const isGoogleAvailable = googleSignInAvailable;
@@ -233,12 +241,14 @@ export function LoginForm({ className, ...props }: DivProps) {
</div>
</Field>
<FieldDescription className="pt-1 text-center">
Não tem uma conta?{" "}
<a href="/signup" className={authLinkClassName}>
Inscreva-se
</a>
</FieldDescription>
{!signupDisabled && (
<FieldDescription className="pt-1 text-center">
Não tem uma conta?{" "}
<a href="/signup" className={authLinkClassName}>
Inscreva-se
</a>
</FieldDescription>
)}
<FieldDescription className="text-center text-sm text-muted-foreground">
<a href="/" className={authLinkClassName}>

View File

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

View File

@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from "next/server";
import { auth } from "@/shared/lib/auth/config";
import { isSignupDisabled } from "@/shared/lib/auth/signup";
// Rotas protegidas que requerem autenticação
const PROTECTED_ROUTES = [
@@ -85,6 +86,22 @@ export default async function proxy(request: NextRequest) {
});
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
if (isAuthenticated && PUBLIC_AUTH_ROUTES.includes(pathname)) {

View File

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