Parcourir la source

feat: add disallow registration and anonymous links option

Resolves #290, #210, #34
poeti8 il y a 5 ans
Parent
commit
b229a3ad88

+ 7 - 0
.example.env

@@ -29,10 +29,17 @@ REDIS_HOST=127.0.0.1
 REDIS_PORT=6379
 REDIS_PASSWORD=
 
+# Disable registration
+DISALLOW_REGISTRATION=false
+
+# Disable anonymous link creation
+DISALLOW_ANONYMOUS_LINKS=false
+
 # The daily limit for each user
 USER_LIMIT_PER_DAY=50
 
 # Create a cooldown for non-logged in users in minutes
+# Would be ignored if DISALLOW_ANONYMOUS_LINKS is set to true
 # Set 0 to disable
 NON_USER_COOLDOWN=0
 

+ 9 - 2
client/components/Header.tsx

@@ -5,6 +5,7 @@ import Router from "next/router";
 import useMedia from "use-media";
 import Link from "next/link";
 
+import { DISALLOW_REGISTRATION } from "../consts";
 import { useStoreState } from "../store";
 import styled from "styled-components";
 import { RowCenterV } from "./Layout";
@@ -55,8 +56,14 @@ const Header: FC = () => {
   const login = !isAuthenticated && (
     <Li>
       <Link href="/login">
-        <ALink href="/login" title="login / signup" forButton>
-          <Button height={[32, 40]}>Login / Sign up</Button>
+        <ALink
+          href="/login"
+          title={!DISALLOW_REGISTRATION ? "login / signup" : "login"}
+          forButton
+        >
+          <Button height={[32, 40]}>
+            {!DISALLOW_REGISTRATION ? "Log in / Sign up" : "Log in"}
+          </Button>
         </ALink>
       </Link>
     </Li>

+ 10 - 0
client/consts/consts.ts

@@ -1,3 +1,13 @@
+import getConfig from "next/config";
+
+const { publicRuntimeConfig } = getConfig();
+
+export const DISALLOW_ANONYMOUS_LINKS =
+  publicRuntimeConfig.DISALLOW_ANONYMOUS_LINKS === "true";
+
+export const DISALLOW_REGISTRATION =
+  publicRuntimeConfig.DISALLOW_REGISTRATION === "true";
+
 export enum API {
   BAN_LINK = "/api/url/admin/ban",
   STATS = "/api/url/stats"

+ 11 - 0
client/pages/index.tsx

@@ -1,5 +1,7 @@
 import React from "react";
+import Router from "next/router";
 
+import { DISALLOW_ANONYMOUS_LINKS } from "../consts";
 import NeedToLogin from "../components/NeedToLogin";
 import Extensions from "../components/Extensions";
 import LinksTable from "../components/LinksTable";
@@ -12,6 +14,15 @@ import { useStoreState } from "../store";
 const Homepage = () => {
   const isAuthenticated = useStoreState(s => s.auth.isAuthenticated);
 
+  if (
+    !isAuthenticated &&
+    DISALLOW_ANONYMOUS_LINKS &&
+    typeof window !== "undefined"
+  ) {
+    Router.push("/login");
+    return null;
+  }
+
   return (
     <AppWrapper>
       <Shortener />

+ 20 - 18
client/pages/login.tsx

@@ -8,6 +8,7 @@ import Link from "next/link";
 import axios from "axios";
 
 import { useStoreState, useStoreActions } from "../store";
+import { APIv2, DISALLOW_REGISTRATION } from "../consts";
 import { ColCenterV } from "../components/Layout";
 import AppWrapper from "../components/AppWrapper";
 import { TextInput } from "../components/Input";
@@ -16,7 +17,6 @@ import { Button } from "../components/Button";
 import Text, { H2 } from "../components/Text";
 import ALink from "../components/ALink";
 import Icon from "../components/Icon";
-import { APIv2 } from "../consts";
 
 const LoginForm = styled(Flex).attrs({
   as: "form",
@@ -77,7 +77,7 @@ const LoginPage = () => {
         }
       }
 
-      if (type === "signup") {
+      if (type === "signup" && !DISALLOW_REGISTRATION) {
         setLoading(s => ({ ...s, signup: true }));
         try {
           await axios.post(APIv2.AuthSignup, { email, password });
@@ -120,7 +120,7 @@ const LoginPage = () => {
               autoFocus
             />
             <Text {...label("password")} as="label" mb={2} bold>
-              Password (min chars: 8):
+              Password{!DISALLOW_REGISTRATION ? " (min chars: 8)" : ""}:
             </Text>
             <TextInput
               {...password("password")}
@@ -135,7 +135,7 @@ const LoginPage = () => {
             <Flex justifyContent="center">
               <Button
                 flex="1 1 auto"
-                mr={["8px", 16]}
+                mr={!DISALLOW_REGISTRATION ? ["8px", 16] : 0}
                 height={[44, 56]}
                 onClick={onSubmit("login")}
               >
@@ -146,20 +146,22 @@ const LoginPage = () => {
                 />
                 Log in
               </Button>
-              <Button
-                flex="1 1 auto"
-                ml={["8px", 16]}
-                height={[44, 56]}
-                color="purple"
-                onClick={onSubmit("signup")}
-              >
-                <Icon
-                  name={loading.signup ? "spinner" : "signup"}
-                  stroke="white"
-                  mr={2}
-                />
-                Sign up
-              </Button>
+              {!DISALLOW_REGISTRATION && (
+                <Button
+                  flex="1 1 auto"
+                  ml={["8px", 16]}
+                  height={[44, 56]}
+                  color="purple"
+                  onClick={onSubmit("signup")}
+                >
+                  <Icon
+                    name={loading.signup ? "spinner" : "signup"}
+                    stroke="white"
+                    mr={2}
+                  />
+                  Sign up
+                </Button>
+              )}
             </Flex>
             <Link href="/reset-password">
               <ALink

+ 3 - 1
next.config.js

@@ -7,6 +7,8 @@ module.exports = {
     DEFAULT_DOMAIN: localEnv && localEnv.DEFAULT_DOMAIN,
     RECAPTCHA_SITE_KEY: localEnv && localEnv.RECAPTCHA_SITE_KEY,
     GOOGLE_ANALYTICS: localEnv && localEnv.GOOGLE_ANALYTICS,
-    REPORT_EMAIL: localEnv && localEnv.REPORT_EMAIL
+    REPORT_EMAIL: localEnv && localEnv.REPORT_EMAIL,
+    DISALLOW_ANONYMOUS_LINKS: localEnv && localEnv.DISALLOW_ANONYMOUS_LINKS,
+    DISALLOW_REGISTRATION: localEnv && localEnv.DISALLOW_REGISTRATION
   }
 };

+ 2 - 0
server/env.ts

@@ -22,6 +22,8 @@ const env = cleanEnv(process.env, {
   USER_LIMIT_PER_DAY: num({ default: 50 }),
   NON_USER_COOLDOWN: num({ default: 10 }),
   DEFAULT_MAX_STATS_PER_LINK: num({ default: 5000 }),
+  DISALLOW_ANONYMOUS_LINKS: bool({ default: false }),
+  DISALLOW_REGISTRATION: bool({ default: false }),
   CUSTOM_DOMAIN_USE_HTTPS: bool({ default: false }),
   JWT_SECRET: str(),
   ADMIN_EMAILS: str({ default: "" }),

+ 10 - 3
server/handlers/auth.ts

@@ -62,6 +62,7 @@ export const apikey = authenticate(
 );
 
 export const cooldown: Handler = async (req, res, next) => {
+  if (env.DISALLOW_ANONYMOUS_LINKS) return next();
   const cooldownConfig = env.NON_USER_COOLDOWN;
   if (req.user || !cooldownConfig) return next();
 
@@ -83,6 +84,7 @@ export const cooldown: Handler = async (req, res, next) => {
 
 export const recaptcha: Handler = async (req, res, next) => {
   if (env.isDev || req.user) return next();
+  if (env.DISALLOW_ANONYMOUS_LINKS) return next();
   if (!env.RECAPTCHA_SECRET_KEY) return next();
 
   const isReCaptchaValid = await axios({
@@ -167,7 +169,7 @@ export const changePassword: Handler = async (req, res) => {
     .send({ message: "Your password has been changed successfully." });
 };
 
-export const generateApiKey = async (req, res) => {
+export const generateApiKey: Handler = async (req, res) => {
   const apikey = nanoid(40);
 
   redis.remove.user(req.user);
@@ -181,7 +183,7 @@ export const generateApiKey = async (req, res) => {
   return res.status(201).send({ apikey });
 };
 
-export const resetPasswordRequest = async (req, res) => {
+export const resetPasswordRequest: Handler = async (req, res) => {
   const [user] = await query.user.update(
     { email: req.body.email },
     {
@@ -199,7 +201,7 @@ export const resetPasswordRequest = async (req, res) => {
   });
 };
 
-export const resetPassword = async (req, res, next) => {
+export const resetPassword: Handler = async (req, res, next) => {
   const { resetPasswordToken } = req.params;
 
   if (resetPasswordToken) {
@@ -218,3 +220,8 @@ export const resetPassword = async (req, res, next) => {
   }
   return next();
 };
+
+export const signupAccess: Handler = (req, res, next) => {
+  if (!env.DISALLOW_REGISTRATION) return next();
+  return res.status(403).send({ message: "Registration is not allowed." });
+};

+ 1 - 0
server/routes/auth.ts

@@ -17,6 +17,7 @@ router.post(
 
 router.post(
   "/signup",
+  auth.signupAccess,
   validators.signup,
   asyncHandler(helpers.verify),
   asyncHandler(auth.signup)

+ 2 - 1
server/routes/links.ts

@@ -6,6 +6,7 @@ import * as validators from "../handlers/validators";
 import * as helpers from "../handlers/helpers";
 import * as link from "../handlers/links";
 import * as auth from "../handlers/auth";
+import env from "../env";
 
 const router = Router();
 
@@ -21,7 +22,7 @@ router.post(
   "/",
   cors(),
   asyncHandler(auth.apikey),
-  asyncHandler(auth.jwtLoose),
+  asyncHandler(env.DISALLOW_ANONYMOUS_LINKS ? auth.jwt : auth.jwtLoose),
   asyncHandler(auth.recaptcha),
   asyncHandler(auth.cooldown),
   validators.createLink,