chore(deps)update deps, remove google analytics

This commit is contained in:
Pouria Ezzati 2022-11-25 19:24:05 +03:30
parent f7c6df2f51
commit 4e672a8b51
No known key found for this signature in database
66 changed files with 14787 additions and 42850 deletions

View File

@ -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

View File

@ -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": {

View File

@ -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
.husky/pre-commit Executable file
View File

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

View File

@ -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

View File

@ -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;
`;

View File

@ -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

View File

@ -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";

View File

@ -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";

View File

@ -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;

View File

@ -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

View File

@ -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({

View File

@ -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 && (

View File

@ -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}

View File

@ -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";

View File

@ -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> = ({

View File

@ -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"

View File

@ -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 ? (

View File

@ -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();
};

View File

@ -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>
);

View File

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

View File

@ -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";

View File

@ -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);
};

View File

@ -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);

View File

@ -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 ? (

View File

@ -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.";

View File

@ -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
}))

View File

@ -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";

View File

@ -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} />;

View File

@ -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 });
}
};

View File

@ -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.

View File

@ -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());
}

View File

@ -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" />

View File

@ -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>

View File

@ -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>

View File

@ -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 />;
};

View File

@ -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;

View File

@ -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();

View File

@ -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 };
};

View File

@ -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>
))}

View File

@ -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 };
};

View File

@ -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>

View File

@ -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
global.d.ts vendored
View File

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

View File

@ -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
}
};

56558
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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": "*"
}
}
}

View File

@ -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 }),

View File

@ -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";

View File

@ -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 (req.query.limit > 50) {
req.query.limit = 50;
if (
typeof req.query.limit !== "undefined" &&
typeof req.query.limit !== "string"
) {
return res.status(400).json({ error: "limit query is not valid." });
}
req.query.all = admin ? all === "true" : false;
if (
typeof req.query.skip !== "undefined" &&
typeof req.query.skip !== "string"
) {
return res.status(400).json({ error: "skip query is not valid." });
}
if (
typeof req.query.search !== "undefined" &&
typeof req.query.search !== "string"
) {
return res.status(400).json({ error: "search query is not valid." });
}
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();
};

View File

@ -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 });
};

View File

@ -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";

View File

@ -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
}

View File

@ -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
}

View File

@ -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",

View File

@ -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",

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

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

View File

@ -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 }));

View File

@ -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))
]);
}
};

View File

@ -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}`);
});
});

View File

@ -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;

View File

@ -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"]
}