feat: allow signup via env var, disabled by default
This commit is contained in:
parent
00d7fb79c3
commit
2dd167e898
12
app/config.server.ts
Normal file
12
app/config.server.ts
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -5,6 +5,10 @@ import { prisma } from "~/db.server";
|
||||||
|
|
||||||
export type { User } from "@prisma/client";
|
export type { User } from "@prisma/client";
|
||||||
|
|
||||||
|
export async function countUsers() {
|
||||||
|
return prisma.user.count();
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUserById(id: User["id"]) {
|
export async function getUserById(id: User["id"]) {
|
||||||
return prisma.user.findUnique({ where: { id } });
|
return prisma.user.findUnique({ where: { id } });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,30 @@
|
||||||
import type { ActionArgs, LoaderArgs, V2_MetaFunction } from "@remix-run/node";
|
import type { ActionArgs, LoaderArgs, V2_MetaFunction } from "@remix-run/node";
|
||||||
import { json, redirect } 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 { 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 { createUserSession, getUserId } from "~/session.server";
|
||||||
import { safeRedirect, validateEmail } from "~/utils";
|
import { safeRedirect, validateEmail } from "~/utils";
|
||||||
|
import { isSignupAllowed } from "~/config.server";
|
||||||
|
|
||||||
export const loader = async ({ request }: LoaderArgs) => {
|
export const loader = async ({ request }: LoaderArgs) => {
|
||||||
const userId = await getUserId(request);
|
const userId = await getUserId(request);
|
||||||
if (userId) return redirect("/");
|
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) => {
|
export const action = async ({ request }: ActionArgs) => {
|
||||||
|
|
@ -19,6 +33,21 @@ export const action = async ({ request }: ActionArgs) => {
|
||||||
const password = formData.get("password");
|
const password = formData.get("password");
|
||||||
const redirectTo = safeRedirect(formData.get("redirectTo"), "/");
|
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)) {
|
if (!validateEmail(email)) {
|
||||||
return json(
|
return json(
|
||||||
{ errors: { email: "Email is invalid", password: null } },
|
{ errors: { email: "Email is invalid", password: null } },
|
||||||
|
|
@ -69,6 +98,7 @@ export default function Join() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const redirectTo = searchParams.get("redirectTo") ?? undefined;
|
const redirectTo = searchParams.get("redirectTo") ?? undefined;
|
||||||
const actionData = useActionData<typeof action>();
|
const actionData = useActionData<typeof action>();
|
||||||
|
const loaderData = useLoaderData<typeof loader>();
|
||||||
const emailRef = useRef<HTMLInputElement>(null);
|
const emailRef = useRef<HTMLInputElement>(null);
|
||||||
const passwordRef = useRef<HTMLInputElement>(null);
|
const passwordRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|
@ -145,20 +175,22 @@ export default function Join() {
|
||||||
>
|
>
|
||||||
Create Account
|
Create Account
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center justify-center">
|
{!loaderData.isFirstUser && (
|
||||||
<div className="text-center text-sm text-gray-500">
|
<div className="flex items-center justify-center">
|
||||||
Already have an account?{" "}
|
<div className="text-center text-sm text-gray-500">
|
||||||
<Link
|
Already have an account?{" "}
|
||||||
className="text-blue-500 underline"
|
<Link
|
||||||
to={{
|
className="text-blue-500 underline"
|
||||||
pathname: "/login",
|
to={{
|
||||||
search: searchParams.toString(),
|
pathname: "/login",
|
||||||
}}
|
search: searchParams.toString(),
|
||||||
>
|
}}
|
||||||
Log in
|
>
|
||||||
</Link>
|
Log in
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,29 @@
|
||||||
import type { ActionArgs, LoaderArgs, V2_MetaFunction } from "@remix-run/node";
|
import type { ActionArgs, LoaderArgs, V2_MetaFunction } from "@remix-run/node";
|
||||||
import { json, redirect } 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 { 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 { createUserSession, getUserId } from "~/session.server";
|
||||||
import { safeRedirect, validateEmail } from "~/utils";
|
import { safeRedirect, validateEmail } from "~/utils";
|
||||||
|
|
||||||
export const loader = async ({ request }: LoaderArgs) => {
|
export const loader = async ({ request }: LoaderArgs) => {
|
||||||
const userId = await getUserId(request);
|
const userId = await getUserId(request);
|
||||||
if (userId) return redirect("/");
|
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) => {
|
export const action = async ({ request }: ActionArgs) => {
|
||||||
|
|
@ -64,6 +77,7 @@ export default function LoginPage() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const redirectTo = searchParams.get("redirectTo") || "/t";
|
const redirectTo = searchParams.get("redirectTo") || "/t";
|
||||||
const actionData = useActionData<typeof action>();
|
const actionData = useActionData<typeof action>();
|
||||||
|
const loaderData = useLoaderData<typeof loader>();
|
||||||
const emailRef = useRef<HTMLInputElement>(null);
|
const emailRef = useRef<HTMLInputElement>(null);
|
||||||
const passwordRef = useRef<HTMLInputElement>(null);
|
const passwordRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|
@ -140,7 +154,7 @@ export default function LoginPage() {
|
||||||
>
|
>
|
||||||
Log in
|
Log in
|
||||||
</button>
|
</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">
|
<div className="flex items-center">
|
||||||
<input
|
<input
|
||||||
id="remember"
|
id="remember"
|
||||||
|
|
@ -155,18 +169,20 @@ export default function LoginPage() {
|
||||||
Remember me
|
Remember me
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center text-sm text-gray-500">
|
{!!loaderData?.ALLOW_USER_SIGNUP && (
|
||||||
Don't have an account?{" "}
|
<div className="mt-8 text-center text-sm text-gray-500 md:mt-0">
|
||||||
<Link
|
Don't have an account?{" "}
|
||||||
className="text-blue-500 underline"
|
<Link
|
||||||
to={{
|
className="text-blue-500 underline"
|
||||||
pathname: "/join",
|
to={{
|
||||||
search: searchParams.toString(),
|
pathname: "/join",
|
||||||
}}
|
search: searchParams.toString(),
|
||||||
>
|
}}
|
||||||
Sign up
|
>
|
||||||
</Link>
|
Sign up
|
||||||
</div>
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue