diff --git a/.env.example b/.env.example
index 1ace8df..d0c274a 100644
--- a/.env.example
+++ b/.env.example
@@ -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=
\ No newline at end of file
+LOGO_DEV_SECRET_KEY=
diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx
index 70f39db..0981e84 100644
--- a/src/app/(auth)/login/page.tsx
+++ b/src/app/(auth)/login/page.tsx
@@ -1,5 +1,6 @@
import { LoginForm } from "@/features/auth/components/login-form";
+import { isSignupDisabled } from "@/shared/lib/auth/signup";
export default function LoginPage() {
- return ;
+ return ;
}
diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx
index 2d6db62..3730eed 100644
--- a/src/app/(auth)/signup/page.tsx
+++ b/src/app/(auth)/signup/page.tsx
@@ -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 ;
}
diff --git a/src/app/(landing-page)/page.tsx b/src/app/(landing-page)/page.tsx
index 44346f7..dfe41a5 100644
--- a/src/app/(landing-page)/page.tsx
+++ b/src/app/(landing-page)/page.tsx
@@ -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
-
-
-
+ {!signupDisabled && (
+
+
+
+ )}
))}
diff --git a/src/features/auth/components/login-form.tsx b/src/features/auth/components/login-form.tsx
index 7ab1c3a..1f84fe6 100644
--- a/src/features/auth/components/login-form.tsx
+++ b/src/features/auth/components/login-form.tsx
@@ -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) {
-
- Não tem uma conta?{" "}
-
- Inscreva-se
-
-
+ {!signupDisabled && (
+
+ Não tem uma conta?{" "}
+
+ Inscreva-se
+
+
+ )}
diff --git a/src/features/landing/components/mobile-nav.tsx b/src/features/landing/components/mobile-nav.tsx
index f2f9fec..ae7a510 100644
--- a/src/features/landing/components/mobile-nav.tsx
+++ b/src/features/landing/components/mobile-nav.tsx
@@ -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
- setOpen(false)}>
-
-
+ {!signupDisabled && (
+ setOpen(false)}>
+
+
+ )}
>
)}
diff --git a/src/proxy.ts b/src/proxy.ts
index 7885ec6..fd80a2f 100644
--- a/src/proxy.ts
+++ b/src/proxy.ts
@@ -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)) {
diff --git a/src/shared/lib/auth/config.ts b/src/shared/lib/auth/config.ts
index a07a9bf..ae5cbf8 100644
--- a/src/shared/lib/auth/config.ts
+++ b/src/shared/lib/auth/config.ts
@@ -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)
diff --git a/src/shared/lib/auth/signup.ts b/src/shared/lib/auth/signup.ts
new file mode 100644
index 0000000..686b91a
--- /dev/null
+++ b/src/shared/lib/auth/signup.ts
@@ -0,0 +1,4 @@
+export function isSignupDisabled(): boolean {
+ const value = process.env.DISABLE_SIGNUP?.trim().toLowerCase();
+ return value === "true";
+}