feat: allow signup via env var, disabled by default

This commit is contained in:
Nicola Zambello 2023-08-07 13:35:07 +02:00
parent 00d7fb79c3
commit 2dd167e898
Signed by: nzambello
GPG key ID: 56E4A92C2C1E50BA
4 changed files with 96 additions and 32 deletions

12
app/config.server.ts Normal file
View file

@ -0,0 +1,12 @@
import { countUsers } from "./models/user.server";
export const ALLOW_USER_SIGNUP = process.env.ALLOW_USER_SIGNUP === "1" || null;
export const isSignupAllowed = async () => {
let isFirstUser = (await countUsers()) === 0;
if (isFirstUser) {
return true;
}
return !!ALLOW_USER_SIGNUP;
};

View file

@ -5,6 +5,10 @@ import { prisma } from "~/db.server";
export type { User } from "@prisma/client";
export async function countUsers() {
return prisma.user.count();
}
export async function getUserById(id: User["id"]) {
return prisma.user.findUnique({ where: { id } });
}

View file

@ -1,16 +1,30 @@
import type { ActionArgs, LoaderArgs, V2_MetaFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, Link, useActionData, useSearchParams } from "@remix-run/react";
import {
Form,
Link,
useActionData,
useLoaderData,
useSearchParams,
} from "@remix-run/react";
import { useEffect, useRef } from "react";
import { createUser, getUserByEmail } from "~/models/user.server";
import { createUser, getUserByEmail, countUsers } from "~/models/user.server";
import { createUserSession, getUserId } from "~/session.server";
import { safeRedirect, validateEmail } from "~/utils";
import { isSignupAllowed } from "~/config.server";
export const loader = async ({ request }: LoaderArgs) => {
const userId = await getUserId(request);
if (userId) return redirect("/");
return json({});
const isFirstUser = (await countUsers()) === 0;
if (!(await isSignupAllowed())) {
return redirect("/login");
}
return json({ isFirstUser });
};
export const action = async ({ request }: ActionArgs) => {
@ -19,6 +33,21 @@ export const action = async ({ request }: ActionArgs) => {
const password = formData.get("password");
const redirectTo = safeRedirect(formData.get("redirectTo"), "/");
const isFirstUser = (await countUsers()) === 0;
if (!isSignupAllowed() && !isFirstUser) {
return json(
{
errors: {
email: "User signup is disabled",
password: null,
confirmPassword: null,
},
},
{ status: 418 }
);
}
if (!validateEmail(email)) {
return json(
{ errors: { email: "Email is invalid", password: null } },
@ -69,6 +98,7 @@ export default function Join() {
const [searchParams] = useSearchParams();
const redirectTo = searchParams.get("redirectTo") ?? undefined;
const actionData = useActionData<typeof action>();
const loaderData = useLoaderData<typeof loader>();
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
@ -145,20 +175,22 @@ export default function Join() {
>
Create Account
</button>
<div className="flex items-center justify-center">
<div className="text-center text-sm text-gray-500">
Already have an account?{" "}
<Link
className="text-blue-500 underline"
to={{
pathname: "/login",
search: searchParams.toString(),
}}
>
Log in
</Link>
{!loaderData.isFirstUser && (
<div className="flex items-center justify-center">
<div className="text-center text-sm text-gray-500">
Already have an account?{" "}
<Link
className="text-blue-500 underline"
to={{
pathname: "/login",
search: searchParams.toString(),
}}
>
Log in
</Link>
</div>
</div>
</div>
)}
</Form>
</div>
</div>

View file

@ -1,16 +1,29 @@
import type { ActionArgs, LoaderArgs, V2_MetaFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, Link, useActionData, useSearchParams } from "@remix-run/react";
import {
Form,
Link,
useActionData,
useLoaderData,
useSearchParams,
} from "@remix-run/react";
import { useEffect, useRef } from "react";
import { isSignupAllowed } from "~/config.server";
import { verifyLogin } from "~/models/user.server";
import { countUsers, verifyLogin } from "~/models/user.server";
import { createUserSession, getUserId } from "~/session.server";
import { safeRedirect, validateEmail } from "~/utils";
export const loader = async ({ request }: LoaderArgs) => {
const userId = await getUserId(request);
if (userId) return redirect("/");
return json({});
const isFirstUser = (await countUsers()) === 0;
if (isFirstUser) return redirect("/signup");
return json({
ALLOW_USER_SIGNUP: await isSignupAllowed(),
});
};
export const action = async ({ request }: ActionArgs) => {
@ -64,6 +77,7 @@ export default function LoginPage() {
const [searchParams] = useSearchParams();
const redirectTo = searchParams.get("redirectTo") || "/t";
const actionData = useActionData<typeof action>();
const loaderData = useLoaderData<typeof loader>();
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
@ -140,7 +154,7 @@ export default function LoginPage() {
>
Log in
</button>
<div className="flex items-center justify-between">
<div className="flex flex-col items-center justify-between md:flex-row">
<div className="flex items-center">
<input
id="remember"
@ -155,18 +169,20 @@ export default function LoginPage() {
Remember me
</label>
</div>
<div className="text-center text-sm text-gray-500">
Don't have an account?{" "}
<Link
className="text-blue-500 underline"
to={{
pathname: "/join",
search: searchParams.toString(),
}}
>
Sign up
</Link>
</div>
{!!loaderData?.ALLOW_USER_SIGNUP && (
<div className="mt-8 text-center text-sm text-gray-500 md:mt-0">
Don't have an account?{" "}
<Link
className="text-blue-500 underline"
to={{
pathname: "/join",
search: searchParams.toString(),
}}
>
Sign up
</Link>
</div>
)}
</div>
</Form>
</div>