mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-09 23:06:01 +00:00
feat(auth): permite bloquear novos cadastros
This commit is contained in:
@@ -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=
|
||||
|
||||
@@ -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()} />;
|
||||
}
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
17
src/proxy.ts
17
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)) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
4
src/shared/lib/auth/signup.ts
Normal file
4
src/shared/lib/auth/signup.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function isSignupDisabled(): boolean {
|
||||
const value = process.env.DISABLE_SIGNUP?.trim().toLowerCase();
|
||||
return value === "true";
|
||||
}
|
||||
Reference in New Issue
Block a user