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
|
# 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
|
||||||
@@ -59,4 +61,4 @@ OPENROUTER_API_KEY=
|
|||||||
# === Logo.dev (Opcional) ===
|
# === Logo.dev (Opcional) ===
|
||||||
# Logos automáticos de estabelecimentos. Cadastre em https://www.logo.dev
|
# Logos automáticos de estabelecimentos. Cadastre em https://www.logo.dev
|
||||||
LOGO_DEV_TOKEN=
|
LOGO_DEV_TOKEN=
|
||||||
LOGO_DEV_SECRET_KEY=
|
LOGO_DEV_SECRET_KEY=
|
||||||
|
|||||||
@@ -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()} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,20 +88,23 @@ export default async function Page() {
|
|||||||
Entrar
|
Entrar
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/signup">
|
{!signupDisabled && (
|
||||||
<Button
|
<Link href="/signup">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
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"
|
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>
|
Começar
|
||||||
</Link>
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<MobileNav
|
<MobileNav
|
||||||
isPublicDomain={isPublicDomain}
|
isPublicDomain={isPublicDomain}
|
||||||
isLoggedIn={!!session?.user}
|
isLoggedIn={!!session?.user}
|
||||||
|
signupDisabled={signupDisabled}
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</NavbarShell>
|
</NavbarShell>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
<FieldDescription className="pt-1 text-center">
|
{!signupDisabled && (
|
||||||
Não tem uma conta?{" "}
|
<FieldDescription className="pt-1 text-center">
|
||||||
<a href="/signup" className={authLinkClassName}>
|
Não tem uma conta?{" "}
|
||||||
Inscreva-se
|
<a href="/signup" className={authLinkClassName}>
|
||||||
</a>
|
Inscreva-se
|
||||||
</FieldDescription>
|
</a>
|
||||||
|
</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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
<Link href="/signup" onClick={() => setOpen(false)}>
|
{!signupDisabled && (
|
||||||
<Button className="w-full gap-2">
|
<Link href="/signup" onClick={() => setOpen(false)}>
|
||||||
Começar
|
<Button className="w-full gap-2">
|
||||||
<RiArrowRightSLine size={16} />
|
Começar
|
||||||
</Button>
|
<RiArrowRightSLine size={16} />
|
||||||
</Link>
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
17
src/proxy.ts
17
src/proxy.ts
@@ -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)) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
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