瀏覽代碼

chore(deps)update deps, remove google analytics

Pouria Ezzati 3 年之前
父節點
當前提交
4e672a8b51
共有 66 個文件被更改,包括 5156 次插入5336 次删除
  1. 0 9
      .docker.env
  2. 8 26
      .eslintrc
  3. 0 9
      .example.env
  4. 4 0
      .husky/pre-commit
  5. 19 4
      client/components/ALink.tsx
  6. 2 2
      client/components/Animation.ts
  7. 7 8
      client/components/AppWrapper.tsx
  8. 1 1
      client/components/Button.tsx
  9. 1 1
      client/components/Divider.tsx
  10. 2 2
      client/components/Extensions.tsx
  11. 2 3
      client/components/Features.tsx
  12. 3 2
      client/components/FeaturesItem.tsx
  13. 4 4
      client/components/Footer.tsx
  14. 57 45
      client/components/Header.tsx
  15. 1 1
      client/components/Icon/Icon.tsx
  16. 10 7
      client/components/Input.tsx
  17. 8 8
      client/components/Layout.tsx
  18. 27 25
      client/components/LinksTable.tsx
  19. 2 2
      client/components/Modal.tsx
  20. 4 6
      client/components/NeedToLogin.tsx
  21. 1 1
      client/components/PageLoading.tsx
  22. 1 1
      client/components/ReCaptcha.tsx
  23. 4 4
      client/components/Settings/SettingsApi.tsx
  24. 2 2
      client/components/Settings/SettingsChangeEmail.tsx
  25. 10 7
      client/components/Settings/SettingsDomain.tsx
  26. 3 3
      client/components/Settings/SettingsPassword.tsx
  27. 18 19
      client/components/Shortener.tsx
  28. 1 1
      client/components/Table.ts
  29. 8 8
      client/components/Text.tsx
  30. 0 25
      client/helpers/analytics.ts
  31. 4 1
      client/next-env.d.ts
  32. 0 11
      client/pages/_app.tsx
  33. 6 5
      client/pages/_document.tsx
  34. 3 4
      client/pages/banned.tsx
  35. 16 18
      client/pages/login.tsx
  36. 3 3
      client/pages/logout.tsx
  37. 2 2
      client/pages/protected/[id].tsx
  38. 2 2
      client/pages/report.tsx
  39. 7 7
      client/pages/reset-password.tsx
  40. 11 17
      client/pages/stats.tsx
  41. 4 4
      client/pages/verify-email.tsx
  42. 15 20
      client/pages/verify.tsx
  43. 16 4
      client/tsconfig.json
  44. 5 0
      global.d.ts
  45. 1 2
      next.config.js
  46. 4654 4761
      package-lock.json
  47. 99 117
      package.json
  48. 0 2
      server/env.ts
  49. 1 1
      server/handlers/auth.ts
  50. 27 7
      server/handlers/helpers.ts
  51. 4 28
      server/handlers/links.ts
  52. 1 1
      server/handlers/validators.ts
  53. 1 1
      server/migrations/20200211220920_constraints.ts
  54. 1 1
      server/migrations/20200510140704_domains.ts
  55. 3 3
      server/queries/domain.ts
  56. 3 3
      server/queries/host.ts
  57. 3 1
      server/queries/index.ts
  58. 3 3
      server/queries/link.ts
  59. 7 11
      server/queries/user.ts
  60. 5 7
      server/queries/visit.ts
  61. 3 1
      server/queues/index.ts
  62. 3 3
      server/queues/visit.ts
  63. 10 22
      server/redis.ts
  64. 2 3
      server/server.ts
  65. 6 6
      server/utils/index.ts
  66. 15 18
      tsconfig.json

+ 0 - 9
.docker.env

@@ -59,15 +59,6 @@ RECAPTCHA_SECRET_KEY=
 # Get it from https://developers.google.com/safe-browsing/v4/get-started
 GOOGLE_SAFE_BROWSING_KEY=
 
-# Google Analytics tracking ID for universal analytics.
-# Example: UA-XXXX-XX
-GOOGLE_ANALYTICS=
-GOOGLE_ANALYTICS_UNIVERSAL=
-
-# Google Analytics tracking ID for universal analytics
-# This one is used for links
-# GOOGLE_ANALYTICS_UNIVERSAL=
-
 # Your email host details to use to send verification emails.
 # More info on http://nodemailer.com/
 # Mail from example "Kutt <support@kutt.it>". Leave empty to use MAIL_USER

+ 8 - 26
.eslintrc

@@ -1,40 +1,22 @@
 {
   "extends": [
-    "eslint:recommended",
-    "plugin:@typescript-eslint/eslint-recommended",
-    "plugin:prettier/recommended"
+    "next/core-web-vitals",
+    "plugin:@typescript-eslint/recommended",
+    "prettier"
   ],
-  "parser": "@typescript-eslint/parser",
   "parserOptions": {
+    "ecmaVersion": "latest",
+    "sourceType": "module",
     "project": ["./tsconfig.json", "./client/tsconfig.json"]
   },
-  "plugins": ["@typescript-eslint"],
+  "plugins": ["@typescript-eslint", "prettier"],
   "rules": {
-    "eqeqeq": ["warn", "always", { "null": "ignore" }],
-    "no-useless-return": "warn",
-    "no-var": "warn",
-    "no-console": "warn",
-    "no-unused-vars": "off",
-    "max-len": ["warn", { "comments": 80 }],
-    "no-param-reassign": 0,
-    "require-atomic-updates": 0,
-    "@typescript-eslint/interface-name-prefix": "off",
-    "@typescript-eslint/no-unused-vars": "off", // "warn" for production
-    "@typescript-eslint/no-explicit-any": "off", // "warn" for production
-    "@typescript-eslint/no-var-requires": "off",
-    "@typescript-eslint/camelcase": "off",
-    "@typescript-eslint/no-object-literal-type-assertion": "off",
-    "@typescript-eslint/no-parameter-properties": "off",
-    "@typescript-eslint/explicit-function-return-type": "off"
+    "@typescript-eslint/no-explicit-any": ["off"]
   },
   "env": {
     "es6": true,
     "browser": true,
-    "node": true,
-    "mocha": true
-  },
-  "globals": {
-    "assert": true
+    "node": true
   },
   "settings": {
     "react": {

+ 0 - 9
.example.env

@@ -62,15 +62,6 @@ RECAPTCHA_SECRET_KEY=
 # Get it from https://developers.google.com/safe-browsing/v4/get-started
 GOOGLE_SAFE_BROWSING_KEY=
 
-# Google Analytics tracking ID for universal analytics.
-# Example: UA-XXXX-XX
-GOOGLE_ANALYTICS=
-GOOGLE_ANALYTICS_UNIVERSAL=
-
-# Google Analytics tracking ID for universal analytics
-# This one is used for links
-# GOOGLE_ANALYTICS_UNIVERSAL=
-
 # Your email host details to use to send verification emails.
 # More info on http://nodemailer.com/
 # Mail from example "Kutt <support@kutt.it>". Leave empty to use MAIL_USER

+ 4 - 0
.husky/pre-commit

@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+npm run lint:nofix

+ 19 - 4
client/components/ALink.tsx

@@ -1,6 +1,8 @@
-import { Box, BoxProps } from "reflexbox/styled-components";
+import { FC } from "react";
+import { Box, BoxProps } from "rebass/styled-components";
 import styled, { css } from "styled-components";
 import { ifProp } from "styled-tools";
+import Link from "next/link";
 
 interface Props extends BoxProps {
   href?: string;
@@ -8,10 +10,9 @@ interface Props extends BoxProps {
   target?: string;
   rel?: string;
   forButton?: boolean;
+  isNextLink?: boolean;
 }
-const ALink = styled(Box).attrs({
-  as: "a"
-})<Props>`
+const StyledBox = styled(Box)<Props>`
   cursor: pointer;
   color: #2196f3;
   border-bottom: 1px dotted transparent;
@@ -28,6 +29,20 @@ const ALink = styled(Box).attrs({
   )}
 `;
 
+export const ALink: FC<Props> = (props) => {
+  if (props.isNextLink) {
+    const { href, target, title, rel, ...rest } = props;
+    return (
+      <Link href={href} target={target} title={title} rel={rel} passHref>
+        <StyledBox as="a" {...rest} />
+      </Link>
+    );
+  }
+  return <StyledBox as="a" {...props} />;
+};
+
+ALink.displayName = "ALink";
+
 ALink.defaultProps = {
   pb: "1px",
   forButton: false

+ 2 - 2
client/components/Animation.ts

@@ -1,5 +1,5 @@
 import { fadeInVertical } from "../helpers/animations";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import styled from "styled-components";
 import { prop } from "styled-tools";
 import { FC } from "react";
@@ -10,7 +10,7 @@ interface Props extends React.ComponentProps<typeof Flex> {
 }
 
 const Animation: FC<Props> = styled(Flex)<Props>`
-  animation: ${props => fadeInVertical(props.offset)}
+  animation: ${(props) => fadeInVertical(props.offset)}
     ${prop("duration", "0.3s")} ease-out;
 `;
 

+ 7 - 8
client/components/AppWrapper.tsx

@@ -1,7 +1,6 @@
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import React, { useEffect } from "react";
 import styled from "styled-components";
-import Router from "next/router";
 
 import { useStoreState, useStoreActions } from "../store";
 import PageLoading from "./PageLoading";
@@ -22,11 +21,11 @@ const Wrapper = styled(Flex)`
 `;
 
 const AppWrapper = ({ children }: { children: any }) => {
-  const isAuthenticated = useStoreState(s => s.auth.isAuthenticated);
-  const logout = useStoreActions(s => s.auth.logout);
-  const fetched = useStoreState(s => s.settings.fetched);
-  const loading = useStoreState(s => s.loading.loading);
-  const getSettings = useStoreActions(s => s.settings.getSettings);
+  const isAuthenticated = useStoreState((s) => s.auth.isAuthenticated);
+  const logout = useStoreActions((s) => s.auth.logout);
+  const fetched = useStoreState((s) => s.settings.fetched);
+  const loading = useStoreState((s) => s.loading.loading);
+  const getSettings = useStoreActions((s) => s.settings.getSettings);
 
   const isVerifyEmailPage =
     typeof window !== "undefined" &&
@@ -36,7 +35,7 @@ const AppWrapper = ({ children }: { children: any }) => {
     if (isAuthenticated && !fetched && !isVerifyEmailPage) {
       getSettings().catch(() => logout());
     }
-  }, [isVerifyEmailPage]);
+  }, [isAuthenticated, fetched, isVerifyEmailPage, getSettings, logout]);
 
   return (
     <Wrapper

+ 1 - 1
client/components/Button.tsx

@@ -1,6 +1,6 @@
 import styled, { css } from "styled-components";
 import { switchProp, prop, ifProp } from "styled-tools";
-import { Flex, BoxProps } from "reflexbox/styled-components";
+import { Flex, BoxProps } from "rebass/styled-components";
 
 interface Props extends BoxProps {
   color?: "purple" | "gray" | "blue" | "red";

+ 1 - 1
client/components/Divider.tsx

@@ -1,4 +1,4 @@
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import styled from "styled-components";
 
 import { Colors } from "../consts";

+ 2 - 2
client/components/Extensions.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import styled from "styled-components";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import SVG from "react-inlinesvg"; // TODO: another solution
 import { Colors } from "../consts";
 import { ColCenterH } from "./Layout";
@@ -62,7 +62,7 @@ const Icon = styled(SVG)`
   width: 18px;
   height: 18px;
   margin-right: 16px;
-  fill: ${props => props.color || "#333"};
+  fill: ${(props) => props.color || "#333"};
 
   @media only screen and (max-width: 768px) {
     width: 13px;

+ 2 - 3
client/components/Features.tsx

@@ -1,11 +1,10 @@
 import React from "react";
-import styled from "styled-components";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 
 import FeaturesItem from "./FeaturesItem";
 import { ColCenterH } from "./Layout";
 import { Colors } from "../consts";
-import Text, { H3 } from "./Text";
+import { H3 } from "./Text";
 
 const Features = () => (
   <ColCenterH

+ 3 - 2
client/components/FeaturesItem.tsx

@@ -1,6 +1,6 @@
-import React, { FC } from "react";
+import React, { FC, ReactNode } from "react";
 import styled from "styled-components";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 
 import { fadeIn } from "../helpers/animations";
 import Icon from "./Icon";
@@ -9,6 +9,7 @@ import { Icons } from "./Icon/Icon";
 interface Props {
   title: string;
   icon: Icons;
+  children?: ReactNode;
 }
 
 const Block = styled(Flex).attrs({

+ 4 - 4
client/components/Footer.tsx

@@ -11,7 +11,7 @@ import Text from "./Text";
 const { publicRuntimeConfig } = getConfig();
 
 const Footer: FC = () => {
-  const { isAuthenticated } = useStoreState(s => s.auth);
+  const { isAuthenticated } = useStoreState((s) => s.auth);
 
   useEffect(() => {
     showRecaptcha();
@@ -27,7 +27,7 @@ const Footer: FC = () => {
       {!isAuthenticated && <ReCaptcha />}
       <Text fontSize={[12, 13]} py={2}>
         Made with love by{" "}
-        <ALink href="//thedevs.network/" title="The Devs">
+        <ALink href="//thedevs.network/" title="The Devs" target="_blank">
           The Devs
         </ALink>
         .{" | "}
@@ -39,11 +39,11 @@ const Footer: FC = () => {
           GitHub
         </ALink>
         {" | "}
-        <ALink href="/terms" title="Terms of Service">
+        <ALink href="/terms" title="Terms of Service" isNextLink>
           Terms of Service
         </ALink>
         {" | "}
-        <ALink href="/report" title="Report abuse">
+        <ALink href="/report" title="Report abuse" isNextLink>
           Report Abuse
         </ALink>
         {publicRuntimeConfig.CONTACT_EMAIL && (

+ 57 - 45
client/components/Header.tsx

@@ -1,9 +1,9 @@
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import getConfig from "next/config";
 import React, { FC } from "react";
 import Router from "next/router";
 import useMedia from "use-media";
-import Link from "next/link";
+import Image from "next/image";
 
 import { DISALLOW_REGISTRATION } from "../consts";
 import { useStoreState } from "../store";
@@ -35,6 +35,7 @@ const LogoImage = styled.div`
     text-decoration: none;
     color: inherit;
     transition: border-color 0.2s ease-out;
+    padding: 0;
   }
 
   @media only screen and (max-width: 488px) {
@@ -43,47 +44,41 @@ const LogoImage = styled.div`
     }
   }
 
-  img {
-    width: 18px;
-    margin-right: 11px;
+  span {
+    margin-right: 10px !important;
   }
 `;
 
 const Header: FC = () => {
-  const { isAuthenticated } = useStoreState(s => s.auth);
+  const { isAuthenticated } = useStoreState((s) => s.auth);
   const isMobile = useMedia({ maxWidth: 640 });
 
   const login = !isAuthenticated && (
     <Li>
-      <Link href="/login">
-        <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>
+      <ALink
+        href="/login"
+        title={!DISALLOW_REGISTRATION ? "login / signup" : "login"}
+        forButton
+        isNextLink
+      >
+        <Button height={[32, 40]}>
+          {!DISALLOW_REGISTRATION ? "Log in / Sign up" : "Log in"}
+        </Button>
+      </ALink>
     </Li>
   );
   const logout = isAuthenticated && (
     <Li>
-      <Link href="/logout">
-        <ALink href="/logout" title="logout" fontSize={[14, 16]}>
-          Log out
-        </ALink>
-      </Link>
+      <ALink href="/logout" title="logout" fontSize={[14, 16]} isNextLink>
+        Log out
+      </ALink>
     </Li>
   );
   const settings = isAuthenticated && (
     <Li>
-      <Link href="/settings">
-        <ALink href="/settings" title="Settings" forButton>
-          <Button height={[32, 40]}>Settings</Button>
-        </ALink>
-      </Link>
+      <ALink href="/settings" title="Settings" forButton isNextLink>
+        <Button height={[32, 40]}>Settings</Button>
+      </ALink>
     </Li>
   );
 
@@ -102,27 +97,36 @@ const Header: FC = () => {
         alignItems={["flex-start", "stretch"]}
       >
         <LogoImage>
-          <a
+          <ALink
             href="/"
             title="Homepage"
-            onClick={e => {
+            onClick={(e) => {
               e.preventDefault();
               if (window.location.pathname !== "/") Router.push("/");
             }}
+            forButton
+            isNextLink
           >
-            <img src="/images/logo.svg" alt="" />
+            <Image
+              src="/images/logo.svg"
+              alt="kutt logo"
+              width={18}
+              height={24}
+            />
             {publicRuntimeConfig.SITE_NAME}
-          </a>
+          </ALink>
         </LogoImage>
+
         {!isMobile && (
           <Flex
             style={{ listStyle: "none" }}
             display={["none", "flex"]}
             alignItems="flex-end"
             as="ul"
-            mb="3px"
             m={0}
-            p={0}
+            px={0}
+            pt={0}
+            pb="2px"
           >
             <Li>
               <ALink
@@ -136,11 +140,14 @@ const Header: FC = () => {
               </ALink>
             </Li>
             <Li>
-              <Link href="/report">
-                <ALink href="/report" title="Report abuse" fontSize={[14, 16]}>
-                  Report
-                </ALink>
-              </Link>
+              <ALink
+                href="/report"
+                title="Report abuse"
+                fontSize={[14, 16]}
+                isNextLink
+              >
+                Report
+              </ALink>
             </Li>
           </Flex>
         )}
@@ -152,15 +159,20 @@ const Header: FC = () => {
         as="ul"
         style={{ listStyle: "none" }}
       >
-        <Li>
-          <Flex display={["flex", "none"]}>
-            <Link href="/report">
-              <ALink href="/report" title="Report" fontSize={[14, 16]}>
+        {isMobile && (
+          <Li>
+            <Flex>
+              <ALink
+                href="/report"
+                title="Report"
+                fontSize={[14, 16]}
+                isNextLink
+              >
                 Report
               </ALink>
-            </Link>
-          </Flex>
-        </Li>
+            </Flex>
+          </Li>
+        )}
         {logout}
         {settings}
         {login}

+ 1 - 1
client/components/Icon/Icon.tsx

@@ -1,4 +1,4 @@
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import styled, { css } from "styled-components";
 import { prop, ifProp } from "styled-tools";
 import React, { FC } from "react";

+ 10 - 7
client/components/Input.tsx

@@ -1,5 +1,5 @@
-import React from 'react';
-import { Flex, BoxProps } from "reflexbox/styled-components";
+import React from "react";
+import { Flex, BoxProps } from "rebass/styled-components";
 import styled, { css, keyframes } from "styled-components";
 import { withProp, prop, ifProp } from "styled-tools";
 import { FC } from "react";
@@ -41,7 +41,7 @@ export const TextInput = styled(Flex).attrs({
   }
 
   ::placeholder {
-    font-size: ${withProp("placeholderSize", s => s[0] || 14)}px;
+    font-size: ${withProp("placeholderSize", (s) => s[0] || 14)}px;
     letter-spacing: 0.05em;
     color: #888;
   }
@@ -50,7 +50,7 @@ export const TextInput = styled(Flex).attrs({
     ::placeholder {
       font-size: ${withProp(
         "placeholderSize",
-        s => s[3] || s[2] || s[1] || s[0] || 16
+        (s) => s[3] || s[2] || s[1] || s[0] || 16
       )}px;
     }
   }
@@ -61,14 +61,14 @@ export const TextInput = styled(Flex).attrs({
     ::placeholder {
       font-size: ${withProp(
         "placeholderSize",
-        s => s[2] || s[1] || s[0] || 15
+        (s) => s[2] || s[1] || s[0] || 15
       )}px;
     }
   }
 
   @media screen and (min-width: 40em) {
     ::placeholder {
-      font-size: ${withProp("placeholderSize", s => s[1] || s[0] || 15)}px;
+      font-size: ${withProp("placeholderSize", (s) => s[1] || s[0] || 15)}px;
     }
   }
 `;
@@ -211,8 +211,11 @@ const CheckboxBox = styled(Flex).attrs({
   )}
 `;
 
-interface CheckboxProps extends ChecknoxInputProps, BoxProps {
+interface CheckboxProps
+  extends ChecknoxInputProps,
+    Omit<BoxProps, "name" | "checked" | "onChange" | "value"> {
   label: string;
+  value?: boolean | string;
 }
 
 export const Checkbox: FC<CheckboxProps> = ({

+ 8 - 8
client/components/Layout.tsx

@@ -1,34 +1,34 @@
 import React from "react";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import { FC } from "react";
 
 type Props = React.ComponentProps<typeof Flex>;
 
-export const Col: FC<Props> = props => (
+export const Col: FC<Props> = (props) => (
   <Flex flexDirection="column" {...props} />
 );
 
-export const RowCenterV: FC<Props> = props => (
+export const RowCenterV: FC<Props> = (props) => (
   <Flex alignItems="center" {...props} />
 );
 
-export const RowCenterH: FC<Props> = props => (
+export const RowCenterH: FC<Props> = (props) => (
   <Flex justifyContent="center" {...props} />
 );
 
-export const RowCenter: FC<Props> = props => (
+export const RowCenter: FC<Props> = (props) => (
   <Flex alignItems="center" justifyContent="center" {...props} />
 );
 
-export const ColCenterV: FC<Props> = props => (
+export const ColCenterV: FC<Props> = (props) => (
   <Flex flexDirection="column" justifyContent="center" {...props} />
 );
 
-export const ColCenterH: FC<Props> = props => (
+export const ColCenterH: FC<Props> = (props) => (
   <Flex flexDirection="column" alignItems="center" {...props} />
 );
 
-export const ColCenter: FC<Props> = props => (
+export const ColCenter: FC<Props> = (props) => (
   <Flex
     flexDirection="column"
     alignItems="center"

+ 27 - 25
client/components/LinksTable.tsx

@@ -2,12 +2,11 @@ import formatDistanceToNow from "date-fns/formatDistanceToNow";
 import { CopyToClipboard } from "react-copy-to-clipboard";
 import React, { FC, useState, useEffect } from "react";
 import { useFormState } from "react-use-form-state";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import styled, { css } from "styled-components";
 import { ifProp } from "styled-tools";
 import getConfig from "next/config";
 import QRCode from "qrcode.react";
-import Link from "next/link";
 import differenceInMilliseconds from "date-fns/differenceInMilliseconds";
 import ms from "ms";
 
@@ -120,9 +119,9 @@ interface EditForm {
 }
 
 const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
-  const isAdmin = useStoreState(s => s.auth.isAdmin);
-  const ban = useStoreActions(s => s.links.ban);
-  const edit = useStoreActions(s => s.links.edit);
+  const isAdmin = useStoreState((s) => s.auth.isAdmin);
+  const ban = useStoreActions((s) => s.links.ban);
+  const edit = useStoreActions((s) => s.links.edit);
   const [banFormState, { checkbox }] = useFormState<BanForm>();
   const [editFormState, { text, label, password }] = useFormState<EditForm>(
     {
@@ -182,7 +181,7 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
   };
 
   const toggleEdit = () => {
-    setShowEdit(s => !s);
+    setShowEdit((s) => !s);
     if (showEdit) editFormState.reset();
     setEditMessage("");
   };
@@ -280,16 +279,19 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
             </>
           )}
           {link.visit_count > 0 && (
-            <Link href={`/stats?id=${link.id}`}>
-              <ALink title="View stats" forButton>
-                <Action
-                  name="pieChart"
-                  stroke={Colors.PieIcon}
-                  strokeWidth="2.5"
-                  backgroundColor={Colors.PieIconBg}
-                />
-              </ALink>
-            </Link>
+            <ALink
+              href={`/stats?id=${link.id}`}
+              title="View stats"
+              forButton
+              isNextLink
+            >
+              <Action
+                name="pieChart"
+                stroke={Colors.PieIcon}
+                strokeWidth="2.5"
+                backgroundColor={Colors.PieIconBg}
+              />
+            </ALink>
           )}
           <Action
             name="qrcode"
@@ -503,7 +505,7 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
           </H2>
           <Text mb={24} textAlign="center">
             Are you sure do you want to ban the link{" "}
-            <Span bold>"{removeProtocol(link.link)}"</Span>?
+            <Span bold>&quot;{removeProtocol(link.link)}&quot;</Span>?
           </Text>
           <RowCenter>
             <Checkbox {...checkbox("user")} label="User" mb={12} />
@@ -546,9 +548,9 @@ interface Form {
 }
 
 const LinksTable: FC = () => {
-  const isAdmin = useStoreState(s => s.auth.isAdmin);
-  const links = useStoreState(s => s.links);
-  const { get, remove } = useStoreActions(s => s.links);
+  const isAdmin = useStoreState((s) => s.auth.isAdmin);
+  const links = useStoreState((s) => s.links);
+  const { get, remove } = useStoreActions((s) => s.links);
   const [tableMessage, setTableMessage] = useState("No links to show.");
   const [deleteModal, setDeleteModal] = useState(-1);
   const [deleteLoading, setDeleteLoading] = useState(false);
@@ -562,12 +564,12 @@ const LinksTable: FC = () => {
   const linkToDelete = links.items[deleteModal];
 
   useEffect(() => {
-    get(options).catch(err =>
+    get(options).catch((err) =>
       setTableMessage(err?.response?.data?.error || "An error occurred.")
     );
-  }, [options.limit, options.skip, options.all]);
+  }, [options, get]);
 
-  const onSubmit = e => {
+  const onSubmit = (e) => {
     e.preventDefault();
     get(options);
   };
@@ -596,7 +598,7 @@ const LinksTable: FC = () => {
       flexShrink={1}
     >
       <Flex as="ul" m={0} p={0} style={{ listStyle: "none" }}>
-        {["10", "25", "50"].map(c => (
+        {["10", "25", "50"].map((c) => (
           <Flex key={c} ml={[10, 12]}>
             <NavButton
               disabled={options.limit === c}
@@ -722,7 +724,7 @@ const LinksTable: FC = () => {
             </H2>
             <Text textAlign="center">
               Are you sure do you want to delete the link{" "}
-              <Span bold>"{removeProtocol(linkToDelete.link)}"</Span>?
+              <Span bold>&quot;{removeProtocol(linkToDelete.link)}&quot;</Span>?
             </Text>
             <Flex justifyContent="center" mt={44}>
               {deleteLoading ? (

+ 2 - 2
client/components/Modal.tsx

@@ -1,4 +1,4 @@
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import styled from "styled-components";
 import React, { FC } from "react";
 import ReactDOM from "react-dom";
@@ -27,7 +27,7 @@ const Wrapper = styled.div`
 const Modal: FC<Props> = ({ children, id, show, closeHandler, ...rest }) => {
   if (!show) return null;
 
-  const onClickOutside = e => {
+  const onClickOutside = (e) => {
     if (e.target.id === id) closeHandler();
   };
 

+ 4 - 6
client/components/NeedToLogin.tsx

@@ -1,7 +1,7 @@
 import React from "react";
 import Link from "next/link";
 import styled from "styled-components";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 
 import { Button } from "./Button";
 import { fadeIn } from "../helpers/animations";
@@ -65,13 +65,11 @@ const NeedToLogin = () => (
       <Title>
         Manage links, set custom <b>domains</b> and view <b>stats</b>.
       </Title>
-      <Link href="/login">
-        <a href="/login" title="login / signup">
-          <Button>Login / Signup</Button>
-        </a>
+      <Link href="/login" title="login / signup">
+        <Button>Login / Signup</Button>
       </Link>
     </Col>
-    <Image src="/images/callout.png" />
+    <Image src="/images/callout.png" alt="callout image" />
   </Wrapper>
 );
 

+ 1 - 1
client/components/PageLoading.tsx

@@ -1,4 +1,4 @@
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import React from "react";
 
 import { Colors } from "../consts";

+ 1 - 1
client/components/ReCaptcha.tsx

@@ -1,4 +1,4 @@
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import getConfig from "next/config";
 import React from "react";
 

+ 4 - 4
client/components/Settings/SettingsApi.tsx

@@ -1,5 +1,5 @@
 import { CopyToClipboard } from "react-copy-to-clipboard";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import React, { FC, useState } from "react";
 import styled from "styled-components";
 
@@ -32,13 +32,13 @@ const SettingsApi: FC = () => {
   const [copied, setCopied] = useCopy();
   const [message, setMessage] = useMessage(1500);
   const [loading, setLoading] = useState(false);
-  const apikey = useStoreState(s => s.settings.apikey);
-  const generateApiKey = useStoreActions(s => s.settings.generateApiKey);
+  const apikey = useStoreState((s) => s.settings.apikey);
+  const generateApiKey = useStoreActions((s) => s.settings.generateApiKey);
 
   const onSubmit = async () => {
     if (loading) return;
     setLoading(true);
-    await generateApiKey().catch(err => setMessage(errorMessage(err)));
+    await generateApiKey().catch((err) => setMessage(errorMessage(err)));
     setLoading(false);
   };
 

+ 2 - 2
client/components/Settings/SettingsChangeEmail.tsx

@@ -1,6 +1,6 @@
 import { useFormState } from "react-use-form-state";
 import React, { FC, useState } from "react";
-import { Flex } from "reflexbox";
+import { Flex } from "rebass";
 import axios from "axios";
 
 import { getAxiosConfig } from "../../utils";
@@ -22,7 +22,7 @@ const SettingsChangeEmail: FC = () => {
     withIds: true
   });
 
-  const onSubmit = async e => {
+  const onSubmit = async (e) => {
     e.preventDefault();
     if (loading) return;
     setLoading(true);

+ 10 - 7
client/components/Settings/SettingsDomain.tsx

@@ -1,5 +1,5 @@
 import { useFormState } from "react-use-form-state";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import React, { FC, useState } from "react";
 import styled from "styled-components";
 import getConfig from "next/config";
@@ -27,10 +27,10 @@ const Td = styled(Flex).attrs({ as: "td", py: 12, px: 3 })`
 `;
 
 const SettingsDomain: FC = () => {
-  const { saveDomain, deleteDomain } = useStoreActions(s => s.settings);
+  const { saveDomain, deleteDomain } = useStoreActions((s) => s.settings);
   const [domainToDelete, setDomainToDelete] = useState<Domain>(null);
   const [deleteLoading, setDeleteLoading] = useState(false);
-  const domains = useStoreState(s => s.settings.domains);
+  const domains = useStoreState((s) => s.settings.domains);
   const [message, setMessage] = useMessage(2000);
   const [loading, setLoading] = useState(false);
   const [modal, setModal] = useState(false);
@@ -39,7 +39,7 @@ const SettingsDomain: FC = () => {
     homepage: string;
   }>(null, { withIds: true });
 
-  const onSubmit = async e => {
+  const onSubmit = async (e) => {
     e.preventDefault();
     setLoading(true);
 
@@ -59,7 +59,7 @@ const SettingsDomain: FC = () => {
 
   const onDelete = async () => {
     setDeleteLoading(true);
-    await deleteDomain(domainToDelete.id).catch(err =>
+    await deleteDomain(domainToDelete.id).catch((err) =>
       setMessage(errorMessage(err, "Couldn't delete the domain."))
     );
     setMessage("Domain has been deleted successfully.", "green");
@@ -91,7 +91,7 @@ const SettingsDomain: FC = () => {
             </tr>
           </thead>
           <tbody>
-            {domains.map(d => (
+            {domains.map((d) => (
               <tr key={d.address}>
                 <Td width={2 / 5}>{d.address}</Td>
                 <Td width={2 / 5}>
@@ -174,7 +174,10 @@ const SettingsDomain: FC = () => {
         </H2>
         <Text textAlign="center">
           Are you sure do you want to delete the domain{" "}
-          <Span bold>"{domainToDelete && domainToDelete.address}"</Span>?
+          <Span bold>
+            &quot;{domainToDelete && domainToDelete.address}&quot;
+          </Span>
+          ?
         </Text>
         <Flex justifyContent="center" mt={44}>
           {deleteLoading ? (

+ 3 - 3
client/components/Settings/SettingsPassword.tsx

@@ -1,5 +1,5 @@
 import { useFormState } from "react-use-form-state";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import React, { FC, useState } from "react";
 import axios from "axios";
 
@@ -20,7 +20,7 @@ const SettingsPassword: FC = () => {
     { withIds: true }
   );
 
-  const onSubmit = async e => {
+  const onSubmit = async (e) => {
     e.preventDefault();
     if (loading) return;
     if (!formState.validity.password) {
@@ -61,7 +61,7 @@ const SettingsPassword: FC = () => {
         <TextInput
           {...password({
             name: "password",
-            validate: value => {
+            validate: (value) => {
               const val = value.trim();
               if (!val || val.length < 8) {
                 return "Password must be at least 8 chars.";

+ 18 - 19
client/components/Shortener.tsx

@@ -1,6 +1,6 @@
 import { CopyToClipboard } from "react-copy-to-clipboard";
 import { useFormState } from "react-use-form-state";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import React, { useState } from "react";
 import styled from "styled-components";
 import getConfig from "next/config";
@@ -62,27 +62,26 @@ interface Form {
 const defaultDomain = publicRuntimeConfig.DEFAULT_DOMAIN;
 
 const Shortener = () => {
-  const { isAuthenticated } = useStoreState(s => s.auth);
-  const domains = useStoreState(s => s.settings.domains);
-  const submit = useStoreActions(s => s.links.submit);
+  const { isAuthenticated } = useStoreState((s) => s.auth);
+  const domains = useStoreState((s) => s.settings.domains);
+  const submit = useStoreActions((s) => s.links.submit);
   const [link, setLink] = useState<Link | null>(null);
   const [message, setMessage] = useMessage(3000);
   const [loading, setLoading] = useState(false);
   const [copied, setCopied] = useCopy();
-  const [formState, { raw, password, text, select, label }] = useFormState<
-    Form
-  >(
-    { showAdvanced: false },
-    {
-      withIds: true,
-      onChange(e, stateValues, nextStateValues) {
-        if (stateValues.showAdvanced && !nextStateValues.showAdvanced) {
-          formState.clear();
-          formState.setField("target", stateValues.target);
+  const [formState, { raw, password, text, select, label }] =
+    useFormState<Form>(
+      { showAdvanced: false },
+      {
+        withIds: true,
+        onChange(e, stateValues, nextStateValues) {
+          if (stateValues.showAdvanced && !nextStateValues.showAdvanced) {
+            formState.clear();
+            formState.setField("target", stateValues.target);
+          }
         }
       }
-    }
-  );
+    );
 
   const submitLink = async (reCaptchaToken?: string) => {
     try {
@@ -97,7 +96,7 @@ const Shortener = () => {
     setLoading(false);
   };
 
-  const onSubmit = async e => {
+  const onSubmit = async (e) => {
     e.preventDefault();
     if (loading) return;
     setCopied(false);
@@ -232,7 +231,7 @@ const Shortener = () => {
       <Checkbox
         {...raw({
           name: "showAdvanced",
-          onChange: e => {
+          onChange: () => {
             if (!isAuthenticated) {
               setMessage(
                 "You need to log in or sign up to use advanced options."
@@ -270,7 +269,7 @@ const Shortener = () => {
                 width={[1, 210, 240]}
                 options={[
                   { key: defaultDomain, value: "" },
-                  ...domains.map(d => ({
+                  ...domains.map((d) => ({
                     key: d.address,
                     value: d.address
                   }))

+ 1 - 1
client/components/Table.ts

@@ -1,4 +1,4 @@
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import styled, { css } from "styled-components";
 import { ifProp, prop } from "styled-tools";
 

+ 8 - 8
client/components/Text.tsx

@@ -1,6 +1,6 @@
 import React from "react";
 import { switchProp, ifNotProp, ifProp } from "styled-tools";
-import { Box, BoxProps } from "reflexbox/styled-components";
+import { Box, BoxProps } from "rebass/styled-components";
 import styled, { css } from "styled-components";
 
 import { FC, CSSProperties } from "react";
@@ -58,10 +58,10 @@ Text.defaultProps = {
 
 export default Text;
 
-export const H1: FC<Props> = props => <Text as="h1" {...props} />;
-export const H2: FC<Props> = props => <Text as="h2" {...props} />;
-export const H3: FC<Props> = props => <Text as="h3" {...props} />;
-export const H4: FC<Props> = props => <Text as="h4" {...props} />;
-export const H5: FC<Props> = props => <Text as="h5" {...props} />;
-export const H6: FC<Props> = props => <Text as="h6" {...props} />;
-export const Span: FC<Props> = props => <Text as="span" {...props} />;
+export const H1: FC<Props> = (props) => <Text as="h1" {...props} />;
+export const H2: FC<Props> = (props) => <Text as="h2" {...props} />;
+export const H3: FC<Props> = (props) => <Text as="h3" {...props} />;
+export const H4: FC<Props> = (props) => <Text as="h4" {...props} />;
+export const H5: FC<Props> = (props) => <Text as="h5" {...props} />;
+export const H6: FC<Props> = (props) => <Text as="h6" {...props} />;
+export const Span: FC<Props> = (props) => <Text as="span" {...props} />;

+ 0 - 25
client/helpers/analytics.ts

@@ -1,25 +0,0 @@
-import getConfig from "next/config";
-import ReactGA from "react-ga";
-
-const { publicRuntimeConfig } = getConfig();
-
-export const initGA = () => {
-  ReactGA.initialize(publicRuntimeConfig.GOOGLE_ANALYTICS);
-};
-
-export const logPageView = () => {
-  ReactGA.set({ page: window.location.pathname });
-  ReactGA.pageview(window.location.pathname);
-};
-
-export const logEvent = (category = "", action = "") => {
-  if (category && action) {
-    ReactGA.event({ category, action });
-  }
-};
-
-export const logException = (description = "", fatal = false) => {
-  if (description) {
-    ReactGA.exception({ description, fatal });
-  }
-};

+ 4 - 1
client/next-env.d.ts

@@ -1,2 +1,5 @@
 /// <reference types="next" />
-/// <reference types="next/types/global" />
+/// <reference types="next/image-types/global" />
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.

+ 0 - 11
client/pages/_app.tsx

@@ -7,11 +7,9 @@ import cookie from "js-cookie";
 import Head from "next/head";
 import React from "react";
 
-import { initGA, logPageView } from "../helpers/analytics";
 import { initializeStore } from "../store";
 import { TokenPayload } from "../types";
 
-const isProd = process.env.NODE_ENV === "production";
 const { publicRuntimeConfig } = getConfig();
 
 // TODO: types
@@ -55,18 +53,9 @@ class MyApp extends App<any> {
       });
     }
 
-    if (isProd) {
-      initGA();
-      logPageView();
-    }
-
     Router.events.on("routeChangeStart", () => loading.show());
     Router.events.on("routeChangeComplete", () => {
       loading.hide();
-
-      if (isProd) {
-        logPageView();
-      }
     });
     Router.events.on("routeChangeError", () => loading.hide());
   }

+ 6 - 5
client/pages/_document.tsx

@@ -12,13 +12,14 @@ interface Props {
 }
 
 class AppDocument extends Document<Props> {
-  static getInitialProps({ renderPage }) {
+  static async getInitialProps(ctx) {
+    const initialProps = await Document.getInitialProps(ctx);
     const sheet = new ServerStyleSheet();
-    const page = renderPage(App => props =>
-      sheet.collectStyles(<App {...props} />)
+    const page = ctx.renderPage(
+      (App) => (props) => sheet.collectStyles(<App {...props} />)
     );
     const styleTags = sheet.getStyleElement();
-    return { ...page, styleTags };
+    return { ...initialProps, ...page, styleTags };
   }
 
   render() {
@@ -35,7 +36,7 @@ class AppDocument extends Document<Props> {
             content={`${publicRuntimeConfig.SITE_NAME} is a free and open source URL shortener with custom domains and stats.`}
           />
           <link
-            href="https://fonts.googleapis.com/css?family=Nunito:300,400,700"
+            href="https://fonts.googleapis.com/css?family=Nunito:300,400,700&display=optional"
             rel="stylesheet"
           />
           <link rel="icon" sizes="196x196" href="/images/favicon-196x196.png" />

+ 3 - 4
client/pages/banned.tsx

@@ -1,5 +1,4 @@
 import getConfig from "next/config";
-import Link from "next/link";
 import React from "react";
 
 import AppWrapper from "../components/AppWrapper";
@@ -24,9 +23,9 @@ const BannedPage = () => {
         <H4 textAlign="center" normal>
           If you noticed a malware/scam link shortened by{" "}
           {publicRuntimeConfig.SITE_NAME},{" "}
-          <Link href="/report">
-            <ALink title="Send report">send us a report</ALink>
-          </Link>
+          <ALink href="/report" title="Send report" isNextLink>
+            send us a report
+          </ALink>
           .
         </H4>
       </Col>

+ 16 - 18
client/pages/login.tsx

@@ -1,10 +1,9 @@
 import { useFormState } from "react-use-form-state";
 import React, { useEffect, useState } from "react";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import emailValidator from "email-validator";
 import styled from "styled-components";
 import Router from "next/router";
-import Link from "next/link";
 import axios from "axios";
 
 import { useStoreState, useStoreActions } from "../store";
@@ -32,8 +31,8 @@ const Email = styled.span`
 `;
 
 const LoginPage = () => {
-  const { isAuthenticated } = useStoreState(s => s.auth);
-  const login = useStoreActions(s => s.auth.login);
+  const { isAuthenticated } = useStoreState((s) => s.auth);
+  const login = useStoreActions((s) => s.auth.login);
   const [error, setError] = useState("");
   const [verifying, setVerifying] = useState(false);
   const [loading, setLoading] = useState({ login: false, signup: false });
@@ -47,7 +46,7 @@ const LoginPage = () => {
   }, [isAuthenticated]);
 
   function onSubmit(type: "login" | "signup") {
-    return async e => {
+    return async (e) => {
       e.preventDefault();
       const { email, password } = formState.values;
 
@@ -68,7 +67,7 @@ const LoginPage = () => {
       setError("");
 
       if (type === "login") {
-        setLoading(s => ({ ...s, login: true }));
+        setLoading((s) => ({ ...s, login: true }));
         try {
           await login(formState.values);
           Router.push("/");
@@ -78,7 +77,7 @@ const LoginPage = () => {
       }
 
       if (type === "signup" && !DISALLOW_REGISTRATION) {
-        setLoading(s => ({ ...s, signup: true }));
+        setLoading((s) => ({ ...s, signup: true }));
         try {
           await axios.post(APIv2.AuthSignup, { email, password });
           setVerifying(true);
@@ -163,17 +162,16 @@ const LoginPage = () => {
                 </Button>
               )}
             </Flex>
-            <Link href="/reset-password">
-              <ALink
-                href="/reset-password"
-                title="Forget password"
-                fontSize={14}
-                alignSelf="flex-start"
-                my={16}
-              >
-                Forgot your password?
-              </ALink>
-            </Link>
+            <ALink
+              href="/reset-password"
+              title="Forget password"
+              fontSize={14}
+              alignSelf="flex-start"
+              my={16}
+              isNextLink
+            >
+              Forgot your password?
+            </ALink>
             <Text color="red" mt={1} normal>
               {error}
             </Text>

+ 3 - 3
client/pages/logout.tsx

@@ -4,14 +4,14 @@ import Router from "next/router";
 import { useStoreActions } from "../store";
 
 const LogoutPage: FC = () => {
-  const logout = useStoreActions(s => s.auth.logout);
-  const reset = useStoreActions(s => s.reset);
+  const logout = useStoreActions((s) => s.auth.logout);
+  const reset = useStoreActions((s) => s.reset);
 
   useEffect(() => {
     logout();
     reset();
     Router.push("/");
-  }, []);
+  }, [logout, reset]);
 
   return <div />;
 };

+ 2 - 2
client/pages/protected/[id].tsx

@@ -1,5 +1,5 @@
 import { useFormState } from "react-use-form-state";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import React, { useState } from "react";
 import { NextPage } from "next";
 import { useRouter } from "next/router";
@@ -23,7 +23,7 @@ const ProtectedPage: NextPage<Props> = () => {
   const [formState, { password }] = useFormState<{ password: string }>();
   const [error, setError] = useState<string>();
 
-  const onSubmit = async e => {
+  const onSubmit = async (e) => {
     e.preventDefault();
     const { password } = formState.values;
 

+ 2 - 2
client/pages/report.tsx

@@ -1,5 +1,5 @@
 import { useFormState } from "react-use-form-state";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import React, { useState } from "react";
 import axios from "axios";
 
@@ -21,7 +21,7 @@ const ReportPage = () => {
   const [loading, setLoading] = useState(false);
   const [message, setMessage] = useMessage(5000);
 
-  const onSubmit = async e => {
+  const onSubmit = async (e) => {
     e.preventDefault();
     setLoading(true);
     setMessage();

+ 7 - 7
client/pages/reset-password.tsx

@@ -1,6 +1,6 @@
 import { useFormState } from "react-use-form-state";
 import React, { useEffect, useState } from "react";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import Router from "next/router";
 import decode from "jwt-decode";
 import { NextPage } from "next";
@@ -16,15 +16,15 @@ import { Col } from "../components/Layout";
 import { TokenPayload } from "../types";
 import { useMessage } from "../hooks";
 import Icon from "../components/Icon";
-import { API, APIv2 } from "../consts";
+import { APIv2 } from "../consts";
 
 interface Props {
   token?: string;
 }
 
 const ResetPassword: NextPage<Props> = ({ token }) => {
-  const auth = useStoreState(s => s.auth);
-  const addAuth = useStoreActions(s => s.auth.add);
+  const auth = useStoreState((s) => s.auth);
+  const addAuth = useStoreActions((s) => s.auth.add);
   const [loading, setLoading] = useState(false);
   const [message, setMessage] = useMessage();
   const [formState, { email, label }] = useFormState<{ email: string }>(null, {
@@ -42,9 +42,9 @@ const ResetPassword: NextPage<Props> = ({ token }) => {
       addAuth(decoded);
       Router.push("/settings");
     }
-  }, []);
+  }, [auth, token, addAuth]);
 
-  const onSubmit = async e => {
+  const onSubmit = async (e) => {
     e.preventDefault();
     if (!formState.validity.email) return;
 
@@ -103,7 +103,7 @@ const ResetPassword: NextPage<Props> = ({ token }) => {
   );
 };
 
-ResetPassword.getInitialProps = async ctx => {
+ResetPassword.getInitialProps = async (ctx) => {
   return { token: ctx.req && (ctx.req as any).token };
 };
 

+ 11 - 17
client/pages/stats.tsx

@@ -1,8 +1,7 @@
-import { Box, Flex } from "reflexbox/styled-components";
+import { Box, Flex } from "rebass/styled-components";
 import React, { useState, useEffect } from "react";
 import formatDate from "date-fns/format";
 import { NextPage } from "next";
-import Link from "next/link";
 import axios from "axios";
 
 import Text, { H1, H2, H4, Span } from "../components/Text";
@@ -23,7 +22,7 @@ interface Props {
 }
 
 const StatsPage: NextPage<Props> = ({ id }) => {
-  const { isAuthenticated } = useStoreState(s => s.auth);
+  const { isAuthenticated } = useStoreState((s) => s.auth);
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState(false);
   const [data, setData] = useState<Record<string, any> | undefined>();
@@ -44,7 +43,7 @@ const StatsPage: NextPage<Props> = ({ id }) => {
         setLoading(false);
         setError(true);
       });
-  }, []);
+  }, [id, isAuthenticated]);
 
   let errorMessage;
 
@@ -61,7 +60,7 @@ const StatsPage: NextPage<Props> = ({ id }) => {
     errorMessage = (
       <Flex mt={3}>
         <Icon name="x" size={32} mr={3} stroke={Colors.TrashIcon} />
-        <H2>Couldn't get stats.</H2>
+        <H2>Couldn&apos;t get stats.</H2>
       </Flex>
     );
   }
@@ -88,10 +87,7 @@ const StatsPage: NextPage<Props> = ({ id }) => {
               </H1>
               <Text fontSize={[13, 14]} textAlign="right">
                 {data.target.length > 80
-                  ? `${data.target
-                      .split("")
-                      .slice(0, 80)
-                      .join("")}...`
+                  ? `${data.target.split("").slice(0, 80).join("")}...`
                   : data.target}
               </Text>
             </Flex>
@@ -187,14 +183,12 @@ const StatsPage: NextPage<Props> = ({ id }) => {
               </Col>
             </Col>
             <Box alignSelf="center" my={64}>
-              <Link href="/">
-                <ALink href="/" title="Back to homepage" forButton>
-                  <Button>
-                    <Icon name="arrowLeft" stroke="white" mr={2} />
-                    Back to homepage
-                  </Button>
-                </ALink>
-              </Link>
+              <ALink href="/" title="Back to homepage" forButton isNextLink>
+                <Button>
+                  <Icon name="arrowLeft" stroke="white" mr={2} />
+                  Back to homepage
+                </Button>
+              </ALink>
             </Box>
           </Col>
         ))}

+ 4 - 4
client/pages/verify-email.tsx

@@ -1,5 +1,5 @@
 import React, { useEffect } from "react";
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import decode from "jwt-decode";
 import { NextPage } from "next";
 import cookie from "js-cookie";
@@ -17,7 +17,7 @@ interface Props {
 }
 
 const VerifyEmail: NextPage<Props> = ({ token }) => {
-  const addAuth = useStoreActions(s => s.auth.add);
+  const addAuth = useStoreActions((s) => s.auth.add);
 
   useEffect(() => {
     if (token) {
@@ -25,7 +25,7 @@ const VerifyEmail: NextPage<Props> = ({ token }) => {
       const decoded: TokenPayload = decode(token);
       addAuth(decoded);
     }
-  }, []);
+  }, [addAuth, token]);
 
   return (
     <AppWrapper>
@@ -48,7 +48,7 @@ const VerifyEmail: NextPage<Props> = ({ token }) => {
   );
 };
 
-VerifyEmail.getInitialProps = async ctx => {
+VerifyEmail.getInitialProps = async (ctx) => {
   return { token: (ctx?.req as any)?.token };
 };
 

+ 15 - 20
client/pages/verify.tsx

@@ -1,9 +1,8 @@
-import { Flex } from "reflexbox/styled-components";
+import { Flex } from "rebass/styled-components";
 import React, { useEffect } from "react";
 import styled from "styled-components";
 import decode from "jwt-decode";
 import cookie from "js-cookie";
-import Link from "next/link";
 
 import AppWrapper from "../components/AppWrapper";
 import { Button } from "../components/Button";
@@ -35,7 +34,7 @@ const Message = styled.p`
 `;
 
 const Verify: NextPage<Props> = ({ token }) => {
-  const addAuth = useStoreActions(s => s.auth.add);
+  const addAuth = useStoreActions((s) => s.auth.add);
 
   useEffect(() => {
     if (token) {
@@ -43,7 +42,7 @@ const Verify: NextPage<Props> = ({ token }) => {
       const payload: TokenPayload = decode(token);
       addAuth(payload);
     }
-  }, []);
+  }, [token, addAuth]);
 
   return (
     <AppWrapper>
@@ -53,14 +52,12 @@ const Verify: NextPage<Props> = ({ token }) => {
             <Icon name="check" size={32} mr={3} stroke={Colors.CheckIcon} />
             <Message>Your account has been verified successfully!</Message>
           </MessageWrapper>
-          <Link href="/">
-            <ALink href="/" forButton>
-              <Button>
-                <Icon name="arrowLeft" stroke="white" mr={2} />
-                Back to homepage
-              </Button>
-            </ALink>
-          </Link>
+          <ALink href="/" forButton isNextLink>
+            <Button>
+              <Icon name="arrowLeft" stroke="white" mr={2} />
+              Back to homepage
+            </Button>
+          </ALink>
         </Col>
       ) : (
         <Col alignItems="center">
@@ -68,14 +65,12 @@ const Verify: NextPage<Props> = ({ token }) => {
             <Icon name="x" size={32} mr={3} stroke={Colors.TrashIcon} />
             <Message>Invalid verification.</Message>
           </MessageWrapper>
-          <Link href="/login">
-            <ALink href="/login" forButton>
-              <Button color="purple">
-                <Icon name="arrowLeft" stroke="white" mr={2} />
-                Back to signup
-              </Button>
-            </ALink>
-          </Link>
+          <ALink href="/login" forButton isNextLink>
+            <Button color="purple">
+              <Icon name="arrowLeft" stroke="white" mr={2} />
+              Back to signup
+            </Button>
+          </ALink>
         </Col>
       )}
     </AppWrapper>

+ 16 - 4
client/tsconfig.json

@@ -1,7 +1,11 @@
 {
   "compilerOptions": {
     "target": "es5",
-    "lib": ["dom", "dom.iterable", "esnext"],
+    "lib": [
+      "dom",
+      "dom.iterable",
+      "esnext"
+    ],
     "allowJs": true,
     "skipLibCheck": true,
     "strict": false,
@@ -12,8 +16,16 @@
     "moduleResolution": "node",
     "resolveJsonModule": true,
     "isolatedModules": true,
-    "jsx": "preserve"
+    "jsx": "preserve",
+    "incremental": true
   },
-  "exclude": ["node_modules"],
-  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "./module.d.ts"]
+  "exclude": [
+    "node_modules"
+  ],
+  "include": [
+    "next-env.d.ts",
+    "**/*.ts",
+    "**/*.tsx",
+    "./module.d.ts"
+  ]
 }

+ 5 - 0
global.d.ts

@@ -151,5 +151,10 @@ declare namespace Express {
     protectedLink?: string;
     token?: string;
     user: UserJoined;
+    context?: {
+      limit: number;
+      skip: number;
+      all: boolean;
+    };
   }
 }

+ 1 - 2
next.config.js

@@ -6,9 +6,8 @@ module.exports = {
     SITE_NAME: localEnv && localEnv.SITE_NAME,
     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,
     DISALLOW_ANONYMOUS_LINKS: localEnv && localEnv.DISALLOW_ANONYMOUS_LINKS,
-    DISALLOW_REGISTRATION: localEnv && localEnv.DISALLOW_REGISTRATION,
+    DISALLOW_REGISTRATION: localEnv && localEnv.DISALLOW_REGISTRATION
   }
 };

文件差異過大導致無法顯示
+ 4654 - 4761
package-lock.json


+ 99 - 117
package.json

@@ -4,7 +4,7 @@
   "description": "Modern URL shortener.",
   "main": "./production-server/server.js",
   "scripts": {
-    "test": "jest",
+    "test": "jest --passWithNoTests",
     "docker:build": "docker build -t kutt .",
     "docker:run": "docker run -p 3000:3000 --env-file .env -d kutt:latest",
     "dev": "npm run migrate && cross-env NODE_ENV=development nodemon server/server.ts",
@@ -14,12 +14,8 @@
     "migrate:make": "knex migrate:make --env production",
     "lint": "eslint server/ --ext .js,.ts --fix",
     "lint:nofix": "eslint server/ --ext .js,.ts",
-    "docs:build": "cd docs/api && tsc generate.ts --resolveJsonModule && node generate && cd ../.."
-  },
-  "husky": {
-    "hooks": {
-      "pre-commit": "npm run lint:nofix"
-    }
+    "docs:build": "cd docs/api && tsc generate.ts --resolveJsonModule && node generate && cd ../..",
+    "prepare": "husky install"
   },
   "repository": {
     "type": "git",
@@ -35,62 +31,59 @@
   },
   "homepage": "https://github.com/TheDevs-Network/kutt#readme",
   "dependencies": {
-    "app-root-path": "^3.0.0",
-    "axios": "^0.21.1",
-    "babel-plugin-inline-react-svg": "^1.1.0",
+    "app-root-path": "^3.1.0",
+    "axios": "^1.1.3",
     "bcryptjs": "^2.4.3",
-    "bull": "^3.12.1",
-    "cookie-parser": "^1.4.4",
+    "bull": "^4.10.1",
+    "cookie-parser": "^1.4.6",
     "cors": "^2.8.5",
     "cross-env": "^7.0.3",
-    "date-fns": "^2.9.0",
-    "dotenv": "^8.2.0",
-    "easy-peasy": "^5.0.3",
-    "email-validator": "^1.2.3",
-    "envalid": "^6.0.0",
-    "express": "^4.17.1",
-    "express-async-handler": "^1.1.4",
-    "express-validator": "^6.3.1",
-    "geoip-lite": "^1.4.0",
-    "helmet": "^3.21.2",
-    "isbot": "^2.5.4",
-    "js-cookie": "^2.2.1",
-    "jsonwebtoken": "^8.4.0",
-    "jwt-decode": "^2.2.0",
-    "knex": "^0.21.1",
-    "morgan": "^1.9.1",
-    "ms": "^2.1.2",
-    "nanoid": "^1.3.4",
-    "neo4j-driver": "^1.7.6",
-    "next": "^9.4.4",
-    "node-cron": "^2.0.3",
-    "nodemailer": "^6.4.2",
-    "p-queue": "^6.2.1",
-    "passport": "^0.4.1",
+    "d3-color": "^3.1.0",
+    "date-fns": "^2.29.3",
+    "dotenv": "^16.0.3",
+    "easy-peasy": "^5.1.0",
+    "email-validator": "^2.0.4",
+    "envalid": "^7.3.1",
+    "express": "^4.18.2",
+    "express-async-handler": "1.1.4",
+    "express-validator": "^6.14.2",
+    "geoip-lite": "^1.4.6",
+    "helmet": "^6.0.0",
+    "ioredis": "^5.2.4",
+    "isbot": "^3.6.3",
+    "js-cookie": "^3.0.1",
+    "jsonwebtoken": "^8.5.1",
+    "jwt-decode": "^3.1.2",
+    "knex": "^2.3.0",
+    "morgan": "^1.10.0",
+    "ms": "^2.1.3",
+    "nanoid": "^2.1.11",
+    "next": "^12.3.3",
+    "node-cron": "^3.0.2",
+    "nodemailer": "^6.8.0",
+    "p-queue": "^7.3.0",
+    "passport": "^0.6.0",
     "passport-jwt": "^4.0.0",
     "passport-local": "^1.0.0",
     "passport-localapikey-update": "^0.6.0",
-    "pg": "^8.2.1",
-    "pg-query-stream": "^2.1.2",
-    "prop-types": "^15.7.2",
-    "qrcode.react": "^0.8.0",
-    "query-string": "^6.10.1",
-    "react": "^16.12.0",
-    "react-copy-to-clipboard": "^5.0.2",
-    "react-dom": "^16.12.0",
-    "react-ga": "^2.7.0",
-    "react-inlinesvg": "^1.2.0",
-    "react-tippy": "^1.3.1",
-    "react-tooltip": "^3.11.2",
-    "react-use-form-state": "^0.12.1",
-    "recharts": "^1.8.5",
-    "redis": "^3.1.1",
-    "reflexbox": "^4.0.6",
+    "pg": "^8.8.0",
+    "pg-query-stream": "^4.2.4",
+    "qrcode.react": "^3.1.0",
+    "query-string": "^7.1.1",
+    "re2": "^1.17.8",
+    "react": "^17.0.2",
+    "react-copy-to-clipboard": "^5.1.0",
+    "react-dom": "^17.0.2",
+    "react-inlinesvg": "^3.0.1",
+    "react-tooltip": "^4.5.0",
+    "react-use-form-state": "^0.13.2",
+    "rebass": "^4.0.7",
+    "recharts": "^2.1.16",
+    "redis": "^4.5.0",
     "signale": "^1.4.0",
-    "styled-components": "^5.0.0",
-    "styled-tools": "^1.7.1",
-    "universal-analytics": "^0.4.20",
-    "url-regex": "^4.1.1",
+    "styled-components": "^5.3.6",
+    "styled-tools": "^1.7.2",
+    "url-regex-safe": "^3.0.0",
     "use-media": "^1.4.0",
     "useragent": "^2.2.1",
     "uuid": "^3.4.0",
@@ -98,73 +91,62 @@
     "winston-daily-rotate-file": "^4.7.1"
   },
   "devDependencies": {
-    "@babel/cli": "^7.8.3",
-    "@babel/core": "^7.12.17",
-    "@babel/node": "^7.8.3",
-    "@babel/preset-env": "^7.12.17",
-    "@babel/register": "^7.8.3",
-    "@testing-library/jest-dom": "^5.11.9",
-    "@testing-library/react": "^11.2.5",
-    "@testing-library/user-event": "^12.8.3",
     "@types/bcryptjs": "^2.4.2",
-    "@types/body-parser": "^1.17.1",
-    "@types/bull": "^3.12.0",
-    "@types/chai": "^4.2.15",
-    "@types/cookie-parser": "^1.4.2",
-    "@types/cors": "^2.8.6",
-    "@types/date-fns": "^2.6.0",
-    "@types/dotenv": "^4.0.3",
-    "@types/express": "^4.17.2",
-    "@types/helmet": "0.0.38",
+    "@types/cookie-parser": "^1.4.3",
+    "@types/cors": "^2.8.12",
+    "@types/express": "^4.17.14",
     "@types/jest": "^26.0.20",
     "@types/jsonwebtoken": "^7.2.8",
-    "@types/jwt-decode": "^2.2.1",
-    "@types/mongodb": "^3.3.14",
     "@types/morgan": "^1.7.37",
     "@types/ms": "^0.7.31",
-    "@types/next": "^9.0.0",
+    "@types/nanoid": "^3.0.0",
+    "@types/node": "^18.11.9",
     "@types/node-cron": "^2.0.2",
-    "@types/nodemailer": "^6.4.0",
-    "@types/pg": "^7.14.1",
-    "@types/pg-query-stream": "^1.0.3",
-    "@types/qrcode.react": "^1.0.0",
-    "@types/react": "^16.9.17",
-    "@types/react-dom": "^16.9.4",
-    "@types/react-tooltip": "^3.11.0",
-    "@types/redis": "^2.8.14",
-    "@types/reflexbox": "^4.0.0",
-    "@types/sinon": "^9.0.10",
+    "@types/nodemailer": "^6.4.6",
+    "@types/pg": "^8.6.5",
+    "@types/qrcode.react": "^1.0.2",
+    "@types/react": "^17.0.52",
+    "@types/react-dom": "^17.0.18",
+    "@types/rebass": "^4.0.10",
+    "@types/signale": "^1.4.4",
     "@types/styled-components": "^5.1.7",
-    "@typescript-eslint/eslint-plugin": "^4.15.2",
-    "@typescript-eslint/parser": "^4.15.2",
-    "babel": "^6.23.0",
-    "babel-cli": "^6.26.0",
-    "babel-core": "^6.26.3",
-    "babel-eslint": "^8.2.6",
-    "babel-jest": "^26.6.3",
-    "babel-plugin-styled-components": "^1.10.6",
-    "babel-preset-env": "^1.7.0",
-    "chai": "^4.3.0",
-    "copyfiles": "^2.2.0",
-    "deep-freeze": "^0.0.1",
-    "eslint": "^5.16.0",
-    "eslint-config-airbnb": "^16.1.0",
-    "eslint-config-prettier": "^6.9.0",
-    "eslint-plugin-import": "^2.20.0",
-    "eslint-plugin-jsx-a11y": "^6.2.3",
-    "eslint-plugin-prettier": "^3.1.2",
-    "eslint-plugin-react": "^7.18.0",
-    "husky": "^0.15.0-rc.13",
-    "jest": "^26.6.3",
-    "mocha": "^5.2.0",
-    "nock": "^9.3.3",
-    "nodemon": "^1.19.4",
-    "prettier": "^1.19.1",
-    "redoc": "^2.0.0-rc.20",
-    "rimraf": "^3.0.0",
-    "sinon": "^6.0.0",
-    "ts-jest": "^26.5.1",
-    "ts-node": "^9.1.1",
-    "typescript": "^4.2.2"
+    "@typescript-eslint/eslint-plugin": "^5.42.1",
+    "@typescript-eslint/parser": "^5.42.1",
+    "copyfiles": "^2.4.1",
+    "eslint": "^8.27.0",
+    "eslint-config-next": "^13.0.3",
+    "eslint-config-prettier": "^8.5.0",
+    "eslint-plugin-prettier": "^4.2.1",
+    "husky": "^8.0.2",
+    "jest": "^29.3.1",
+    "nodemon": "^2.0.20",
+    "prettier": "^2.7.1",
+    "redoc": "^2.0.0",
+    "rimraf": "^3.0.2",
+    "ts-node": "^10.9.1",
+    "typescript": "^4.8.4"
+  },
+  "overrides": {
+    "react-use-form-state": {
+      "react": "*",
+      "react-dom": "*"
+    },
+    "redoc": {
+      "react": "*",
+      "react-dom": "*"
+    },
+    "use-media": {
+      "react": "*",
+      "react-dom": "*"
+    },
+    "react-transition-group": {
+      "react": "*",
+      "react-dom": "*"
+    },
+    "recharts": {
+      "react": "*",
+      "react-dom": "*",
+      "d3-color": "*"
+    }
   }
 }

+ 0 - 2
server/env.ts

@@ -31,8 +31,6 @@ const env = cleanEnv(process.env, {
   RECAPTCHA_SITE_KEY: str({ default: "" }),
   RECAPTCHA_SECRET_KEY: str({ default: "" }),
   GOOGLE_SAFE_BROWSING_KEY: str({ default: "" }),
-  GOOGLE_ANALYTICS: str({ default: "" }),
-  GOOGLE_ANALYTICS_UNIVERSAL: str({ default: "" }),
   MAIL_HOST: str(),
   MAIL_PORT: num(),
   MAIL_SECURE: bool({ default: false }),

+ 1 - 1
server/handlers/auth.ts

@@ -3,7 +3,7 @@ import { Handler } from "express";
 import passport from "passport";
 import bcrypt from "bcryptjs";
 import nanoid from "nanoid";
-import uuid from "uuid/v4";
+import { v4 as uuid } from "uuid";
 import axios from "axios";
 
 import { CustomError } from "../utils";

+ 27 - 7
server/handlers/helpers.ts

@@ -12,7 +12,7 @@ export const ip: Handler = (req, res, next) => {
   return next();
 };
 
-export const error: ErrorRequestHandler = (error, req, res, next) => {
+export const error: ErrorRequestHandler = (error, _req, res, _next) => {
   logger.error(error);
 
   if (env.isDev) {
@@ -36,17 +36,37 @@ export const verify = (req, res, next) => {
 };
 
 export const query: Handler = (req, res, next) => {
-  const { limit, skip, all } = req.query;
   const { admin } = req.user || {};
 
-  req.query.limit = parseInt(limit) || 10;
-  req.query.skip = parseInt(skip) || 0;
+  if (
+    typeof req.query.limit !== "undefined" &&
+    typeof req.query.limit !== "string"
+  ) {
+    return res.status(400).json({ error: "limit query is not valid." });
+  }
+
+  if (
+    typeof req.query.skip !== "undefined" &&
+    typeof req.query.skip !== "string"
+  ) {
+    return res.status(400).json({ error: "skip query is not valid." });
+  }
 
-  if (req.query.limit > 50) {
-    req.query.limit = 50;
+  if (
+    typeof req.query.search !== "undefined" &&
+    typeof req.query.search !== "string"
+  ) {
+    return res.status(400).json({ error: "search query is not valid." });
   }
 
-  req.query.all = admin ? all === "true" : false;
+  const limit = parseInt(req.query.limit) || 10;
+  const skip = parseInt(req.query.skip) || 0;
+
+  req.context = {
+    limit: limit > 50 ? 50 : limit,
+    skip,
+    all: admin ? req.query.all === "true" : false
+  };
 
   next();
 };

+ 4 - 28
server/handlers/links.ts

@@ -1,4 +1,3 @@
-import ua from "universal-analytics";
 import { Handler } from "express";
 import { promisify } from "util";
 import bcrypt from "bcryptjs";
@@ -19,7 +18,8 @@ import env from "../env";
 const dnsLookup = promisify(dns.lookup);
 
 export const get: Handler = async (req, res) => {
-  const { limit, skip, search, all } = req.query;
+  const { limit, skip, all } = req.context;
+  const search = req.query.search as string;
   const userId = req.user.id;
 
   const match = {
@@ -310,19 +310,7 @@ export const redirect = (app: ReturnType<typeof next>): Handler => async (
     });
   }
 
-  // 8. Create Google Analytics visit
-  if (env.GOOGLE_ANALYTICS_UNIVERSAL && !isBot) {
-    ua(env.GOOGLE_ANALYTICS_UNIVERSAL)
-      .pageview({
-        dp: `/${address}`,
-        ua: req.headers["user-agent"],
-        uip: req.realIP,
-        aip: 1
-      })
-      .send();
-  }
-
-  // 10. Redirect to target
+  // 8. Redirect to target
   return res.redirect(link.target);
 };
 
@@ -353,19 +341,7 @@ export const redirectProtected: Handler = async (req, res) => {
     });
   }
 
-  // 5. Create Google Analytics visit
-  if (env.GOOGLE_ANALYTICS_UNIVERSAL) {
-    ua(env.GOOGLE_ANALYTICS_UNIVERSAL)
-      .pageview({
-        dp: `/${link.address}`,
-        ua: req.headers["user-agent"],
-        uip: req.realIP,
-        aip: 1
-      })
-      .send();
-  }
-
-  // 6. Send target
+  // 5. Send target
   return res.status(200).send({ target: link.target });
 };
 

+ 1 - 1
server/handlers/validators.ts

@@ -1,6 +1,6 @@
 import { body, param } from "express-validator";
 import { isAfter, subDays, subHours, addMilliseconds } from "date-fns";
-import urlRegex from "url-regex";
+import urlRegex from "url-regex-safe";
 import { promisify } from "util";
 import bcrypt from "bcryptjs";
 import axios from "axios";

+ 1 - 1
server/migrations/20200211220920_constraints.ts

@@ -37,6 +37,6 @@ export async function up(knex: Knex): Promise<any> {
   ]);
 }
 
-export async function down(knex: Knex): Promise<any> {
+export async function down(): Promise<any> {
   // do nothing
 }

+ 1 - 1
server/migrations/20200510140704_domains.ts

@@ -21,6 +21,6 @@ export async function up(knex: Knex): Promise<any> {
   ]);
 }
 
-export async function down(knex: Knex): Promise<any> {
+export async function down(): Promise<any> {
   // do nothing
 }

+ 3 - 3
server/queries/domain.ts

@@ -1,9 +1,9 @@
-import * as redis from "../redis";
+import redisClient, * as redis from "../redis";
 import knex from "../knex";
 
 export const find = async (match: Partial<Domain>): Promise<Domain> => {
   if (match.address) {
-    const cachedDomain = await redis.get(redis.key.domain(match.address));
+    const cachedDomain = await redisClient.get(redis.key.domain(match.address));
     if (cachedDomain) return JSON.parse(cachedDomain);
   }
 
@@ -12,7 +12,7 @@ export const find = async (match: Partial<Domain>): Promise<Domain> => {
     .first();
 
   if (domain) {
-    redis.set(
+    redisClient.set(
       redis.key.domain(domain.address),
       JSON.stringify(domain),
       "EX",

+ 3 - 3
server/queries/host.ts

@@ -1,4 +1,4 @@
-import * as redis from "../redis";
+import redisClient, * as redis from "../redis";
 import knex from "../knex";
 
 interface Add extends Partial<Host> {
@@ -7,7 +7,7 @@ interface Add extends Partial<Host> {
 
 export const find = async (match: Partial<Host>): Promise<Host> => {
   if (match.address) {
-    const cachedHost = await redis.get(redis.key.host(match.address));
+    const cachedHost = await redisClient.get(redis.key.host(match.address));
     if (cachedHost) return JSON.parse(cachedHost);
   }
 
@@ -16,7 +16,7 @@ export const find = async (match: Partial<Host>): Promise<Host> => {
     .first();
 
   if (host) {
-    redis.set(
+    redisClient.set(
       redis.key.host(host.address),
       JSON.stringify(host),
       "EX",

+ 3 - 1
server/queries/index.ts

@@ -5,7 +5,7 @@ import * as user from "./user";
 import * as host from "./host";
 import * as ip from "./ip";
 
-export default {
+const queries = {
   domain,
   host,
   ip,
@@ -13,3 +13,5 @@ export default {
   user,
   visit
 };
+
+export default queries;

+ 3 - 3
server/queries/link.ts

@@ -1,7 +1,7 @@
 import bcrypt from "bcryptjs";
 
 import { CustomError } from "../utils";
-import * as redis from "../redis";
+import redisClient, * as redis from "../redis";
 import knex from "../knex";
 
 const selectable = [
@@ -96,7 +96,7 @@ export const get = async (match: Partial<Link>, params: GetParams) => {
 export const find = async (match: Partial<Link>): Promise<Link> => {
   if (match.address && match.domain_id) {
     const key = redis.key.link(match.address, match.domain_id);
-    const cachedLink = await redis.get(key);
+    const cachedLink = await redisClient.get(key);
     if (cachedLink) return JSON.parse(cachedLink);
   }
 
@@ -108,7 +108,7 @@ export const find = async (match: Partial<Link>): Promise<Link> => {
 
   if (link) {
     const key = redis.key.link(link.address, link.domain_id);
-    redis.set(key, JSON.stringify(link), "EX", 60 * 60 * 2);
+    redisClient.set(key, JSON.stringify(link), "EX", 60 * 60 * 2);
   }
 
   return link;

+ 7 - 11
server/queries/user.ts

@@ -1,27 +1,25 @@
-import uuid from "uuid/v4";
+import { v4 as uuid } from "uuid";
 import { addMinutes } from "date-fns";
 
-import * as redis from "../redis";
+import redisCLient, * as redis from "../redis";
 import knex from "../knex";
 
 export const find = async (match: Partial<User>) => {
   if (match.email || match.apikey) {
     const key = redis.key.user(match.email || match.apikey);
-    const cachedUser = await redis.get(key);
+    const cachedUser = await redisCLient.get(key);
     if (cachedUser) return JSON.parse(cachedUser) as User;
   }
 
-  const user = await knex<User>("users")
-    .where(match)
-    .first();
+  const user = await knex<User>("users").where(match).first();
 
   if (user) {
     const emailKey = redis.key.user(user.email);
-    redis.set(emailKey, JSON.stringify(user), "EX", 60 * 60 * 1);
+    redisCLient.set(emailKey, JSON.stringify(user), "EX", 60 * 60 * 1);
 
     if (user.apikey) {
       const apikeyKey = redis.key.user(user.apikey);
-      redis.set(apikeyKey, JSON.stringify(user), "EX", 60 * 60 * 1);
+      redisCLient.set(apikeyKey, JSON.stringify(user), "EX", 60 * 60 * 1);
     }
   }
 
@@ -75,9 +73,7 @@ export const update = async (match: Match<User>, update: Partial<User>) => {
 };
 
 export const remove = async (user: User) => {
-  const deletedUser = await knex<User>("users")
-    .where("id", user.id)
-    .delete();
+  const deletedUser = await knex<User>("users").where("id", user.id).delete();
 
   redis.remove.user(user);
 

+ 5 - 7
server/queries/visit.ts

@@ -1,7 +1,7 @@
 import { isAfter, subDays, set } from "date-fns";
 
 import * as utils from "../utils";
-import * as redis from "../redis";
+import redisClient, * as redis from "../redis";
 import knex from "../knex";
 
 interface Add {
@@ -81,7 +81,7 @@ interface IGetStatsResponse {
 export const find = async (match: Partial<Visit>, total: number) => {
   if (match.link_id) {
     const key = redis.key.stats(match.link_id);
-    const cached = await redis.get(key);
+    const cached = await redisClient.get(key);
     if (cached) return JSON.parse(cached);
   }
 
@@ -104,9 +104,7 @@ export const find = async (match: Partial<Visit>, total: number) => {
     }
   };
 
-  const visitsStream: any = knex<Visit>("visits")
-    .where(match)
-    .stream();
+  const visitsStream: any = knex<Visit>("visits").where(match).stream();
   const nowUTC = utils.getUTCDate();
   const now = new Date();
 
@@ -118,7 +116,7 @@ export const find = async (match: Partial<Visit>, total: number) => {
       );
       if (isIncluded) {
         const diffFunction = utils.getDifferenceFunction(type);
-        const diff = diffFunction(now, visit.created_at);
+        const diff = diffFunction(now, new Date(visit.created_at));
         const index = stats[type].views.length - diff - 1;
         const view = stats[type].views[index];
         const period = stats[type].stats;
@@ -238,7 +236,7 @@ export const find = async (match: Partial<Visit>, total: number) => {
   if (match.link_id) {
     const cacheTime = utils.getStatsCacheTime(total);
     const key = redis.key.stats(match.link_id);
-    redis.set(key, JSON.stringify(response), "EX", cacheTime);
+    redisClient.set(key, JSON.stringify(response), "EX", cacheTime);
   }
 
   return response;

+ 3 - 1
server/queues/index.ts

@@ -1,5 +1,7 @@
 import { visit } from "./queues";
 
-export default {
+const queues = {
   visit
 };
+
+export default queues;

+ 3 - 3
server/queues/visit.ts

@@ -7,12 +7,12 @@ import { getStatsLimit, removeWww } from "../utils";
 
 const browsersList = ["IE", "Firefox", "Chrome", "Opera", "Safari", "Edge"];
 const osList = ["Windows", "Mac OS", "Linux", "Android", "iOS"];
-const filterInBrowser = agent => item =>
+const filterInBrowser = (agent) => (item) =>
   agent.family.toLowerCase().includes(item.toLocaleLowerCase());
-const filterInOs = agent => item =>
+const filterInOs = (agent) => (item) =>
   agent.os.family.toLowerCase().includes(item.toLocaleLowerCase());
 
-export default function({ data }) {
+export default function visit({ data }) {
   const tasks = [];
 
   tasks.push(query.link.incrementVisit({ id: data.link.id }));

+ 10 - 22
server/redis.ts

@@ -1,29 +1,15 @@
-import { promisify } from "util";
-import redis from "redis";
+import Redis from "ioredis";
 
 import env from "./env";
 
-const client = redis.createClient({
+const client = new Redis({
   host: env.REDIS_HOST,
   port: env.REDIS_PORT,
   db: env.REDIS_DB,
   ...(env.REDIS_PASSWORD && { password: env.REDIS_PASSWORD })
 });
 
-export const get: (key: string) => Promise<any> = promisify(client.get).bind(
-  client
-);
-
-export const set: (
-  key: string,
-  value: string,
-  ex?: string,
-  exValue?: number
-) => Promise<any> = promisify(client.set).bind(client);
-
-export const del: (key: string) => Promise<any> = promisify(client.del).bind(
-  client
-);
+export default client;
 
 export const key = {
   link: (address: string, domain_id?: number, user_id?: number) =>
@@ -37,19 +23,21 @@ export const key = {
 export const remove = {
   domain: (domain?: Domain) => {
     if (!domain) return;
-    del(key.domain(domain.address));
+    return client.del(key.domain(domain.address));
   },
   host: (host?: Host) => {
     if (!host) return;
-    del(key.host(host.address));
+    return client.del(key.host(host.address));
   },
   link: (link?: Link) => {
     if (!link) return;
-    del(key.link(link.address, link.domain_id));
+    return client.del(key.link(link.address, link.domain_id));
   },
   user: (user?: User) => {
     if (!user) return;
-    del(key.user(user.email));
-    del(key.user(user.apikey));
+    return Promise.all([
+      client.del(key.user(user.email)),
+      client.del(key.user(user.apikey))
+    ]);
   }
 };

+ 2 - 3
server/server.ts

@@ -30,7 +30,7 @@ app.prepare().then(async () => {
     server.use(morgan("combined", { stream }));
   }
 
-  server.use(helmet());
+  server.use(helmet({ contentSecurityPolicy: false }));
   server.use(cookieParser());
   server.use(express.json());
   server.use(express.urlencoded({ extended: true }));
@@ -68,8 +68,7 @@ app.prepare().then(async () => {
   // Handler everything else by Next.js
   server.get("*", (req, res) => handle(req, res));
 
-  server.listen(port, err => {
-    if (err) throw err;
+  server.listen(port, () => {
     console.log(`> Ready on http://localhost:${port}`);
   });
 });

+ 6 - 6
server/utils/index.ts

@@ -1,5 +1,5 @@
 import ms from "ms";
-import generate from "nanoid/generate";
+import nanoid from "nanoid";
 import JWT from "jsonwebtoken";
 import {
   differenceInDays,
@@ -24,7 +24,7 @@ export class CustomError extends Error {
 
 export const isAdmin = (email: string): boolean =>
   env.ADMIN_EMAILS.split(",")
-    .map(e => e.trim())
+    .map((e) => e.trim())
     .includes(email);
 
 export const signToken = (user: UserJoined) =>
@@ -41,7 +41,7 @@ export const signToken = (user: UserJoined) =>
   );
 
 export const generateId = async (domain_id: number = null) => {
-  const address = generate(
+  const address = nanoid(
     "abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ23456789",
     env.LINK_LENGTH
   );
@@ -79,9 +79,9 @@ export const getStatsCacheTime = (total?: number): number => {
 };
 
 export const statsObjectToArray = (obj: Stats) => {
-  const objToArr = key =>
+  const objToArr = (key) =>
     Array.from(Object.keys(obj[key]))
-      .map(name => ({
+      .map((name) => ({
         name,
         value: obj[key][name]
       }))
@@ -97,7 +97,7 @@ export const statsObjectToArray = (obj: Stats) => {
 
 export const getDifferenceFunction = (
   type: "lastDay" | "lastWeek" | "lastMonth" | "allTime"
-): Function => {
+) => {
   if (type === "lastDay") return differenceInHours;
   if (type === "lastWeek") return differenceInDays;
   if (type === "lastMonth") return differenceInDays;

+ 15 - 18
tsconfig.json

@@ -1,19 +1,16 @@
 {
-	"compilerOptions": {
-		"target": "es2018",
-		"module": "commonjs",
-		"sourceMap": true,
-		"outDir": "production-server",
-		"noUnusedLocals": true,
-		"resolveJsonModule": true,
-		"esModuleInterop": true,
-		"noEmit": false,
-		"emitDecoratorMetadata": true,
-		"experimentalDecorators": true,
-		"strict": false
-	},
-	"include": [
-		"global.d.ts",
-		"server"
-	]
-}
+  "compilerOptions": {
+    "target": "es2018",
+    "module": "commonjs",
+    "sourceMap": true,
+    "outDir": "production-server",
+    "noUnusedLocals": true,
+    "resolveJsonModule": true,
+    "esModuleInterop": true,
+    "noEmit": false,
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "strict": false
+  },
+  "include": ["global.d.ts", "server"]
+}

部分文件因文件數量過多而無法顯示