chore(deps)update deps, remove google analytics
This commit is contained in:
parent
f7c6df2f51
commit
4e672a8b51
@ -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
|
||||
|
34
.eslintrc
34
.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": {
|
||||
|
@ -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
4
.husky/pre-commit
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run lint:nofix
|
@ -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
|
||||
|
@ -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;
|
||||
`;
|
||||
|
||||
|
@ -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,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,4 +1,4 @@
|
||||
import { Flex } from "reflexbox/styled-components";
|
||||
import { Flex } from "rebass/styled-components";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Colors } from "../consts";
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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({
|
||||
|
@ -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 && (
|
||||
|
@ -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
|
||||
isNextLink
|
||||
>
|
||||
<Button height={[32, 40]}>
|
||||
{!DISALLOW_REGISTRATION ? "Log in / Sign up" : "Log in"}
|
||||
</Button>
|
||||
</ALink>
|
||||
</Link>
|
||||
</Li>
|
||||
);
|
||||
const logout = isAuthenticated && (
|
||||
<Li>
|
||||
<Link href="/logout">
|
||||
<ALink href="/logout" title="logout" fontSize={[14, 16]}>
|
||||
<ALink href="/logout" title="logout" fontSize={[14, 16]} isNextLink>
|
||||
Log out
|
||||
</ALink>
|
||||
</Link>
|
||||
</Li>
|
||||
);
|
||||
const settings = isAuthenticated && (
|
||||
<Li>
|
||||
<Link href="/settings">
|
||||
<ALink href="/settings" title="Settings" forButton>
|
||||
<ALink href="/settings" title="Settings" forButton isNextLink>
|
||||
<Button height={[32, 40]}>Settings</Button>
|
||||
</ALink>
|
||||
</Link>
|
||||
</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]}>
|
||||
<ALink
|
||||
href="/report"
|
||||
title="Report abuse"
|
||||
fontSize={[14, 16]}
|
||||
isNextLink
|
||||
>
|
||||
Report
|
||||
</ALink>
|
||||
</Link>
|
||||
</Li>
|
||||
</Flex>
|
||||
)}
|
||||
@ -152,15 +159,20 @@ const Header: FC = () => {
|
||||
as="ul"
|
||||
style={{ listStyle: "none" }}
|
||||
>
|
||||
{isMobile && (
|
||||
<Li>
|
||||
<Flex display={["flex", "none"]}>
|
||||
<Link href="/report">
|
||||
<ALink href="/report" title="Report" fontSize={[14, 16]}>
|
||||
<Flex>
|
||||
<ALink
|
||||
href="/report"
|
||||
title="Report"
|
||||
fontSize={[14, 16]}
|
||||
isNextLink
|
||||
>
|
||||
Report
|
||||
</ALink>
|
||||
</Link>
|
||||
</Flex>
|
||||
</Li>
|
||||
)}
|
||||
{logout}
|
||||
{settings}
|
||||
{login}
|
||||
|
@ -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";
|
||||
|
@ -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> = ({
|
||||
|
@ -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"
|
||||
|
@ -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,8 +279,12 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
|
||||
</>
|
||||
)}
|
||||
{link.visit_count > 0 && (
|
||||
<Link href={`/stats?id=${link.id}`}>
|
||||
<ALink title="View stats" forButton>
|
||||
<ALink
|
||||
href={`/stats?id=${link.id}`}
|
||||
title="View stats"
|
||||
forButton
|
||||
isNextLink
|
||||
>
|
||||
<Action
|
||||
name="pieChart"
|
||||
stroke={Colors.PieIcon}
|
||||
@ -289,7 +292,6 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
|
||||
backgroundColor={Colors.PieIconBg}
|
||||
/>
|
||||
</ALink>
|
||||
</Link>
|
||||
)}
|
||||
<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>"{removeProtocol(link.link)}"</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>"{removeProtocol(linkToDelete.link)}"</Span>?
|
||||
</Text>
|
||||
<Flex justifyContent="center" mt={44}>
|
||||
{deleteLoading ? (
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
|
@ -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">
|
||||
<Link href="/login" title="login / signup">
|
||||
<Button>Login / Signup</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</Col>
|
||||
<Image src="/images/callout.png" />
|
||||
<Image src="/images/callout.png" alt="callout image" />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
|
@ -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,4 +1,4 @@
|
||||
import { Flex } from "reflexbox/styled-components";
|
||||
import { Flex } from "rebass/styled-components";
|
||||
import getConfig from "next/config";
|
||||
import React from "react";
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
"{domainToDelete && domainToDelete.address}"
|
||||
</Span>
|
||||
?
|
||||
</Text>
|
||||
<Flex justifyContent="center" mt={44}>
|
||||
{deleteLoading ? (
|
||||
|
@ -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.";
|
||||
|
@ -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,16 +62,15 @@ 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
|
||||
>(
|
||||
const [formState, { raw, password, text, select, label }] =
|
||||
useFormState<Form>(
|
||||
{ showAdvanced: false },
|
||||
{
|
||||
withIds: true,
|
||||
@ -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,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";
|
||||
|
||||
|
@ -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} />;
|
||||
|
@ -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 });
|
||||
}
|
||||
};
|
5
client/next-env.d.ts
vendored
5
client/next-env.d.ts
vendored
@ -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.
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
isNextLink
|
||||
>
|
||||
Forgot your password?
|
||||
</ALink>
|
||||
</Link>
|
||||
<Text color="red" mt={1} normal>
|
||||
{error}
|
||||
</Text>
|
||||
|
@ -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 />;
|
||||
};
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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 };
|
||||
};
|
||||
|
||||
|
@ -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'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>
|
||||
<ALink href="/" title="Back to homepage" forButton isNextLink>
|
||||
<Button>
|
||||
<Icon name="arrowLeft" stroke="white" mr={2} />
|
||||
Back to homepage
|
||||
</Button>
|
||||
</ALink>
|
||||
</Link>
|
||||
</Box>
|
||||
</Col>
|
||||
))}
|
||||
|
@ -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 };
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
<ALink href="/" forButton isNextLink>
|
||||
<Button>
|
||||
<Icon name="arrowLeft" stroke="white" mr={2} />
|
||||
Back to homepage
|
||||
</Button>
|
||||
</ALink>
|
||||
</Link>
|
||||
</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>
|
||||
<ALink href="/login" forButton isNextLink>
|
||||
<Button color="purple">
|
||||
<Icon name="arrowLeft" stroke="white" mr={2} />
|
||||
Back to signup
|
||||
</Button>
|
||||
</ALink>
|
||||
</Link>
|
||||
</Col>
|
||||
)}
|
||||
</AppWrapper>
|
||||
|
@ -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
5
global.d.ts
vendored
@ -151,5 +151,10 @@ declare namespace Express {
|
||||
protectedLink?: string;
|
||||
token?: string;
|
||||
user: UserJoined;
|
||||
context?: {
|
||||
limit: number;
|
||||
skip: number;
|
||||
all: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
};
|
||||
|
56528
package-lock.json
generated
56528
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
216
package.json
216
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": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 }),
|
||||
|
@ -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";
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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,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";
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { visit } from "./queues";
|
||||
|
||||
export default {
|
||||
const queues = {
|
||||
visit
|
||||
};
|
||||
|
||||
export default queues;
|
||||
|
@ -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 }));
|
||||
|
@ -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))
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
@ -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}`);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -12,8 +12,5 @@
|
||||
"experimentalDecorators": true,
|
||||
"strict": false
|
||||
},
|
||||
"include": [
|
||||
"global.d.ts",
|
||||
"server"
|
||||
]
|
||||
"include": ["global.d.ts", "server"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user