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"; +}