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 # Get it from https://developers.google.com/safe-browsing/v4/get-started
GOOGLE_SAFE_BROWSING_KEY= 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. # Your email host details to use to send verification emails.
# More info on http://nodemailer.com/ # More info on http://nodemailer.com/
# Mail from example "Kutt <support@kutt.it>". Leave empty to use MAIL_USER # Mail from example "Kutt <support@kutt.it>". Leave empty to use MAIL_USER

View File

@ -1,40 +1,22 @@
{ {
"extends": [ "extends": [
"eslint:recommended", "next/core-web-vitals",
"plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended" "prettier"
], ],
"parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": ["./tsconfig.json", "./client/tsconfig.json"] "project": ["./tsconfig.json", "./client/tsconfig.json"]
}, },
"plugins": ["@typescript-eslint"], "plugins": ["@typescript-eslint", "prettier"],
"rules": { "rules": {
"eqeqeq": ["warn", "always", { "null": "ignore" }], "@typescript-eslint/no-explicit-any": ["off"]
"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"
}, },
"env": { "env": {
"es6": true, "es6": true,
"browser": true, "browser": true,
"node": true, "node": true
"mocha": true
},
"globals": {
"assert": true
}, },
"settings": { "settings": {
"react": { "react": {

View File

@ -62,15 +62,6 @@ RECAPTCHA_SECRET_KEY=
# Get it from https://developers.google.com/safe-browsing/v4/get-started # Get it from https://developers.google.com/safe-browsing/v4/get-started
GOOGLE_SAFE_BROWSING_KEY= 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. # Your email host details to use to send verification emails.
# More info on http://nodemailer.com/ # More info on http://nodemailer.com/
# Mail from example "Kutt <support@kutt.it>". Leave empty to use MAIL_USER # 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 styled, { css } from "styled-components";
import { ifProp } from "styled-tools"; import { ifProp } from "styled-tools";
import Link from "next/link";
interface Props extends BoxProps { interface Props extends BoxProps {
href?: string; href?: string;
@ -8,10 +10,9 @@ interface Props extends BoxProps {
target?: string; target?: string;
rel?: string; rel?: string;
forButton?: boolean; forButton?: boolean;
isNextLink?: boolean;
} }
const ALink = styled(Box).attrs({ const StyledBox = styled(Box)<Props>`
as: "a"
})<Props>`
cursor: pointer; cursor: pointer;
color: #2196f3; color: #2196f3;
border-bottom: 1px dotted transparent; 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 = { ALink.defaultProps = {
pb: "1px", pb: "1px",
forButton: false forButton: false

View File

@ -1,5 +1,5 @@
import { fadeInVertical } from "../helpers/animations"; import { fadeInVertical } from "../helpers/animations";
import { Flex } from "reflexbox/styled-components"; import { Flex } from "rebass/styled-components";
import styled from "styled-components"; import styled from "styled-components";
import { prop } from "styled-tools"; import { prop } from "styled-tools";
import { FC } from "react"; import { FC } from "react";
@ -10,7 +10,7 @@ interface Props extends React.ComponentProps<typeof Flex> {
} }
const Animation: FC<Props> = styled(Flex)<Props>` const Animation: FC<Props> = styled(Flex)<Props>`
animation: ${props => fadeInVertical(props.offset)} animation: ${(props) => fadeInVertical(props.offset)}
${prop("duration", "0.3s")} ease-out; ${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 React, { useEffect } from "react";
import styled from "styled-components"; import styled from "styled-components";
import Router from "next/router";
import { useStoreState, useStoreActions } from "../store"; import { useStoreState, useStoreActions } from "../store";
import PageLoading from "./PageLoading"; import PageLoading from "./PageLoading";
@ -22,11 +21,11 @@ const Wrapper = styled(Flex)`
`; `;
const AppWrapper = ({ children }: { children: any }) => { const AppWrapper = ({ children }: { children: any }) => {
const isAuthenticated = useStoreState(s => s.auth.isAuthenticated); const isAuthenticated = useStoreState((s) => s.auth.isAuthenticated);
const logout = useStoreActions(s => s.auth.logout); const logout = useStoreActions((s) => s.auth.logout);
const fetched = useStoreState(s => s.settings.fetched); const fetched = useStoreState((s) => s.settings.fetched);
const loading = useStoreState(s => s.loading.loading); const loading = useStoreState((s) => s.loading.loading);
const getSettings = useStoreActions(s => s.settings.getSettings); const getSettings = useStoreActions((s) => s.settings.getSettings);
const isVerifyEmailPage = const isVerifyEmailPage =
typeof window !== "undefined" && typeof window !== "undefined" &&
@ -36,7 +35,7 @@ const AppWrapper = ({ children }: { children: any }) => {
if (isAuthenticated && !fetched && !isVerifyEmailPage) { if (isAuthenticated && !fetched && !isVerifyEmailPage) {
getSettings().catch(() => logout()); getSettings().catch(() => logout());
} }
}, [isVerifyEmailPage]); }, [isAuthenticated, fetched, isVerifyEmailPage, getSettings, logout]);
return ( return (
<Wrapper <Wrapper

View File

@ -1,6 +1,6 @@
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { switchProp, prop, ifProp } from "styled-tools"; 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 { interface Props extends BoxProps {
color?: "purple" | "gray" | "blue" | "red"; 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 styled from "styled-components";
import { Colors } from "../consts"; import { Colors } from "../consts";

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import Text from "./Text";
const { publicRuntimeConfig } = getConfig(); const { publicRuntimeConfig } = getConfig();
const Footer: FC = () => { const Footer: FC = () => {
const { isAuthenticated } = useStoreState(s => s.auth); const { isAuthenticated } = useStoreState((s) => s.auth);
useEffect(() => { useEffect(() => {
showRecaptcha(); showRecaptcha();
@ -27,7 +27,7 @@ const Footer: FC = () => {
{!isAuthenticated && <ReCaptcha />} {!isAuthenticated && <ReCaptcha />}
<Text fontSize={[12, 13]} py={2}> <Text fontSize={[12, 13]} py={2}>
Made with love by{" "} Made with love by{" "}
<ALink href="//thedevs.network/" title="The Devs"> <ALink href="//thedevs.network/" title="The Devs" target="_blank">
The Devs The Devs
</ALink> </ALink>
.{" | "} .{" | "}
@ -39,11 +39,11 @@ const Footer: FC = () => {
GitHub GitHub
</ALink> </ALink>
{" | "} {" | "}
<ALink href="/terms" title="Terms of Service"> <ALink href="/terms" title="Terms of Service" isNextLink>
Terms of Service Terms of Service
</ALink> </ALink>
{" | "} {" | "}
<ALink href="/report" title="Report abuse"> <ALink href="/report" title="Report abuse" isNextLink>
Report Abuse Report Abuse
</ALink> </ALink>
{publicRuntimeConfig.CONTACT_EMAIL && ( {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 getConfig from "next/config";
import React, { FC } from "react"; import React, { FC } from "react";
import Router from "next/router"; import Router from "next/router";
import useMedia from "use-media"; import useMedia from "use-media";
import Link from "next/link"; import Image from "next/image";
import { DISALLOW_REGISTRATION } from "../consts"; import { DISALLOW_REGISTRATION } from "../consts";
import { useStoreState } from "../store"; import { useStoreState } from "../store";
@ -35,6 +35,7 @@ const LogoImage = styled.div`
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
transition: border-color 0.2s ease-out; transition: border-color 0.2s ease-out;
padding: 0;
} }
@media only screen and (max-width: 488px) { @media only screen and (max-width: 488px) {
@ -43,47 +44,41 @@ const LogoImage = styled.div`
} }
} }
img { span {
width: 18px; margin-right: 10px !important;
margin-right: 11px;
} }
`; `;
const Header: FC = () => { const Header: FC = () => {
const { isAuthenticated } = useStoreState(s => s.auth); const { isAuthenticated } = useStoreState((s) => s.auth);
const isMobile = useMedia({ maxWidth: 640 }); const isMobile = useMedia({ maxWidth: 640 });
const login = !isAuthenticated && ( const login = !isAuthenticated && (
<Li> <Li>
<Link href="/login"> <ALink
<ALink href="/login"
href="/login" title={!DISALLOW_REGISTRATION ? "login / signup" : "login"}
title={!DISALLOW_REGISTRATION ? "login / signup" : "login"} forButton
forButton isNextLink
> >
<Button height={[32, 40]}> <Button height={[32, 40]}>
{!DISALLOW_REGISTRATION ? "Log in / Sign up" : "Log in"} {!DISALLOW_REGISTRATION ? "Log in / Sign up" : "Log in"}
</Button> </Button>
</ALink> </ALink>
</Link>
</Li> </Li>
); );
const logout = isAuthenticated && ( const logout = isAuthenticated && (
<Li> <Li>
<Link href="/logout"> <ALink href="/logout" title="logout" fontSize={[14, 16]} isNextLink>
<ALink href="/logout" title="logout" fontSize={[14, 16]}> Log out
Log out </ALink>
</ALink>
</Link>
</Li> </Li>
); );
const settings = isAuthenticated && ( const settings = isAuthenticated && (
<Li> <Li>
<Link href="/settings"> <ALink href="/settings" title="Settings" forButton isNextLink>
<ALink href="/settings" title="Settings" forButton> <Button height={[32, 40]}>Settings</Button>
<Button height={[32, 40]}>Settings</Button> </ALink>
</ALink>
</Link>
</Li> </Li>
); );
@ -102,27 +97,36 @@ const Header: FC = () => {
alignItems={["flex-start", "stretch"]} alignItems={["flex-start", "stretch"]}
> >
<LogoImage> <LogoImage>
<a <ALink
href="/" href="/"
title="Homepage" title="Homepage"
onClick={e => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
if (window.location.pathname !== "/") Router.push("/"); 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} {publicRuntimeConfig.SITE_NAME}
</a> </ALink>
</LogoImage> </LogoImage>
{!isMobile && ( {!isMobile && (
<Flex <Flex
style={{ listStyle: "none" }} style={{ listStyle: "none" }}
display={["none", "flex"]} display={["none", "flex"]}
alignItems="flex-end" alignItems="flex-end"
as="ul" as="ul"
mb="3px"
m={0} m={0}
p={0} px={0}
pt={0}
pb="2px"
> >
<Li> <Li>
<ALink <ALink
@ -136,11 +140,14 @@ const Header: FC = () => {
</ALink> </ALink>
</Li> </Li>
<Li> <Li>
<Link href="/report"> <ALink
<ALink href="/report" title="Report abuse" fontSize={[14, 16]}> href="/report"
Report title="Report abuse"
</ALink> fontSize={[14, 16]}
</Link> isNextLink
>
Report
</ALink>
</Li> </Li>
</Flex> </Flex>
)} )}
@ -152,15 +159,20 @@ const Header: FC = () => {
as="ul" as="ul"
style={{ listStyle: "none" }} style={{ listStyle: "none" }}
> >
<Li> {isMobile && (
<Flex display={["flex", "none"]}> <Li>
<Link href="/report"> <Flex>
<ALink href="/report" title="Report" fontSize={[14, 16]}> <ALink
href="/report"
title="Report"
fontSize={[14, 16]}
isNextLink
>
Report Report
</ALink> </ALink>
</Link> </Flex>
</Flex> </Li>
</Li> )}
{logout} {logout}
{settings} {settings}
{login} {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 styled, { css } from "styled-components";
import { prop, ifProp } from "styled-tools"; import { prop, ifProp } from "styled-tools";
import React, { FC } from "react"; import React, { FC } from "react";

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { Flex } from "reflexbox/styled-components"; import { Flex } from "rebass/styled-components";
import React from "react"; import React from "react";
import { Colors } from "../consts"; 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 getConfig from "next/config";
import React from "react"; import React from "react";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { switchProp, ifNotProp, ifProp } from "styled-tools"; 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 styled, { css } from "styled-components";
import { FC, CSSProperties } from "react"; import { FC, CSSProperties } from "react";
@ -58,10 +58,10 @@ Text.defaultProps = {
export default Text; export default Text;
export const H1: FC<Props> = props => <Text as="h1" {...props} />; export const H1: FC<Props> = (props) => <Text as="h1" {...props} />;
export const H2: FC<Props> = props => <Text as="h2" {...props} />; export const H2: FC<Props> = (props) => <Text as="h2" {...props} />;
export const H3: FC<Props> = props => <Text as="h3" {...props} />; export const H3: FC<Props> = (props) => <Text as="h3" {...props} />;
export const H4: FC<Props> = props => <Text as="h4" {...props} />; export const H4: FC<Props> = (props) => <Text as="h4" {...props} />;
export const H5: FC<Props> = props => <Text as="h5" {...props} />; export const H5: FC<Props> = (props) => <Text as="h5" {...props} />;
export const H6: FC<Props> = props => <Text as="h6" {...props} />; export const H6: FC<Props> = (props) => <Text as="h6" {...props} />;
export const Span: FC<Props> = props => <Text as="span" {...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" />
/// <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 Head from "next/head";
import React from "react"; import React from "react";
import { initGA, logPageView } from "../helpers/analytics";
import { initializeStore } from "../store"; import { initializeStore } from "../store";
import { TokenPayload } from "../types"; import { TokenPayload } from "../types";
const isProd = process.env.NODE_ENV === "production";
const { publicRuntimeConfig } = getConfig(); const { publicRuntimeConfig } = getConfig();
// TODO: types // TODO: types
@ -55,18 +53,9 @@ class MyApp extends App<any> {
}); });
} }
if (isProd) {
initGA();
logPageView();
}
Router.events.on("routeChangeStart", () => loading.show()); Router.events.on("routeChangeStart", () => loading.show());
Router.events.on("routeChangeComplete", () => { Router.events.on("routeChangeComplete", () => {
loading.hide(); loading.hide();
if (isProd) {
logPageView();
}
}); });
Router.events.on("routeChangeError", () => loading.hide()); Router.events.on("routeChangeError", () => loading.hide());
} }

View File

@ -12,13 +12,14 @@ interface Props {
} }
class AppDocument extends Document<Props> { class AppDocument extends Document<Props> {
static getInitialProps({ renderPage }) { static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
const sheet = new ServerStyleSheet(); const sheet = new ServerStyleSheet();
const page = renderPage(App => props => const page = ctx.renderPage(
sheet.collectStyles(<App {...props} />) (App) => (props) => sheet.collectStyles(<App {...props} />)
); );
const styleTags = sheet.getStyleElement(); const styleTags = sheet.getStyleElement();
return { ...page, styleTags }; return { ...initialProps, ...page, styleTags };
} }
render() { 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.`} content={`${publicRuntimeConfig.SITE_NAME} is a free and open source URL shortener with custom domains and stats.`}
/> />
<link <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" rel="stylesheet"
/> />
<link rel="icon" sizes="196x196" href="/images/favicon-196x196.png" /> <link rel="icon" sizes="196x196" href="/images/favicon-196x196.png" />

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { useFormState } from "react-use-form-state"; 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 React, { useState } from "react";
import axios from "axios"; import axios from "axios";
@ -21,7 +21,7 @@ const ReportPage = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [message, setMessage] = useMessage(5000); const [message, setMessage] = useMessage(5000);
const onSubmit = async e => { const onSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
setMessage(); setMessage();

View File

@ -1,6 +1,6 @@
import { useFormState } from "react-use-form-state"; import { useFormState } from "react-use-form-state";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Flex } from "reflexbox/styled-components"; import { Flex } from "rebass/styled-components";
import Router from "next/router"; import Router from "next/router";
import decode from "jwt-decode"; import decode from "jwt-decode";
import { NextPage } from "next"; import { NextPage } from "next";
@ -16,15 +16,15 @@ import { Col } from "../components/Layout";
import { TokenPayload } from "../types"; import { TokenPayload } from "../types";
import { useMessage } from "../hooks"; import { useMessage } from "../hooks";
import Icon from "../components/Icon"; import Icon from "../components/Icon";
import { API, APIv2 } from "../consts"; import { APIv2 } from "../consts";
interface Props { interface Props {
token?: string; token?: string;
} }
const ResetPassword: NextPage<Props> = ({ token }) => { const ResetPassword: NextPage<Props> = ({ token }) => {
const auth = useStoreState(s => s.auth); const auth = useStoreState((s) => s.auth);
const addAuth = useStoreActions(s => s.auth.add); const addAuth = useStoreActions((s) => s.auth.add);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [message, setMessage] = useMessage(); const [message, setMessage] = useMessage();
const [formState, { email, label }] = useFormState<{ email: string }>(null, { const [formState, { email, label }] = useFormState<{ email: string }>(null, {
@ -42,9 +42,9 @@ const ResetPassword: NextPage<Props> = ({ token }) => {
addAuth(decoded); addAuth(decoded);
Router.push("/settings"); Router.push("/settings");
} }
}, []); }, [auth, token, addAuth]);
const onSubmit = async e => { const onSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (!formState.validity.email) return; 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 }; 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 React, { useState, useEffect } from "react";
import formatDate from "date-fns/format"; import formatDate from "date-fns/format";
import { NextPage } from "next"; import { NextPage } from "next";
import Link from "next/link";
import axios from "axios"; import axios from "axios";
import Text, { H1, H2, H4, Span } from "../components/Text"; import Text, { H1, H2, H4, Span } from "../components/Text";
@ -23,7 +22,7 @@ interface Props {
} }
const StatsPage: NextPage<Props> = ({ id }) => { const StatsPage: NextPage<Props> = ({ id }) => {
const { isAuthenticated } = useStoreState(s => s.auth); const { isAuthenticated } = useStoreState((s) => s.auth);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [data, setData] = useState<Record<string, any> | undefined>(); const [data, setData] = useState<Record<string, any> | undefined>();
@ -44,7 +43,7 @@ const StatsPage: NextPage<Props> = ({ id }) => {
setLoading(false); setLoading(false);
setError(true); setError(true);
}); });
}, []); }, [id, isAuthenticated]);
let errorMessage; let errorMessage;
@ -61,7 +60,7 @@ const StatsPage: NextPage<Props> = ({ id }) => {
errorMessage = ( errorMessage = (
<Flex mt={3}> <Flex mt={3}>
<Icon name="x" size={32} mr={3} stroke={Colors.TrashIcon} /> <Icon name="x" size={32} mr={3} stroke={Colors.TrashIcon} />
<H2>Couldn't get stats.</H2> <H2>Couldn&apos;t get stats.</H2>
</Flex> </Flex>
); );
} }
@ -88,10 +87,7 @@ const StatsPage: NextPage<Props> = ({ id }) => {
</H1> </H1>
<Text fontSize={[13, 14]} textAlign="right"> <Text fontSize={[13, 14]} textAlign="right">
{data.target.length > 80 {data.target.length > 80
? `${data.target ? `${data.target.split("").slice(0, 80).join("")}...`
.split("")
.slice(0, 80)
.join("")}...`
: data.target} : data.target}
</Text> </Text>
</Flex> </Flex>
@ -187,14 +183,12 @@ const StatsPage: NextPage<Props> = ({ id }) => {
</Col> </Col>
</Col> </Col>
<Box alignSelf="center" my={64}> <Box alignSelf="center" my={64}>
<Link href="/"> <ALink href="/" title="Back to homepage" forButton isNextLink>
<ALink href="/" title="Back to homepage" forButton> <Button>
<Button> <Icon name="arrowLeft" stroke="white" mr={2} />
<Icon name="arrowLeft" stroke="white" mr={2} /> Back to homepage
Back to homepage </Button>
</Button> </ALink>
</ALink>
</Link>
</Box> </Box>
</Col> </Col>
))} ))}

View File

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

View File

@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": false, "strict": false,
@ -12,8 +16,16 @@
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve" "jsx": "preserve",
"incremental": true
}, },
"exclude": ["node_modules"], "exclude": [
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "./module.d.ts"] "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; protectedLink?: string;
token?: string; token?: string;
user: UserJoined; user: UserJoined;
context?: {
limit: number;
skip: number;
all: boolean;
};
} }
} }

View File

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

View File

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

View File

@ -12,7 +12,7 @@ export const ip: Handler = (req, res, next) => {
return next(); return next();
}; };
export const error: ErrorRequestHandler = (error, req, res, next) => { export const error: ErrorRequestHandler = (error, _req, res, _next) => {
logger.error(error); logger.error(error);
if (env.isDev) { if (env.isDev) {
@ -36,17 +36,37 @@ export const verify = (req, res, next) => {
}; };
export const query: Handler = (req, res, next) => { export const query: Handler = (req, res, next) => {
const { limit, skip, all } = req.query;
const { admin } = req.user || {}; const { admin } = req.user || {};
req.query.limit = parseInt(limit) || 10; if (
req.query.skip = parseInt(skip) || 0; typeof req.query.limit !== "undefined" &&
typeof req.query.limit !== "string"
if (req.query.limit > 50) { ) {
req.query.limit = 50; 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(); next();
}; };

View File

@ -1,4 +1,3 @@
import ua from "universal-analytics";
import { Handler } from "express"; import { Handler } from "express";
import { promisify } from "util"; import { promisify } from "util";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
@ -19,7 +18,8 @@ import env from "../env";
const dnsLookup = promisify(dns.lookup); const dnsLookup = promisify(dns.lookup);
export const get: Handler = async (req, res) => { 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 userId = req.user.id;
const match = { const match = {
@ -310,19 +310,7 @@ export const redirect = (app: ReturnType<typeof next>): Handler => async (
}); });
} }
// 8. Create Google Analytics visit // 8. Redirect to target
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
return res.redirect(link.target); return res.redirect(link.target);
}; };
@ -353,19 +341,7 @@ export const redirectProtected: Handler = async (req, res) => {
}); });
} }
// 5. Create Google Analytics visit // 5. Send target
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
return res.status(200).send({ target: link.target }); return res.status(200).send({ target: link.target });
}; };

View File

@ -1,6 +1,6 @@
import { body, param } from "express-validator"; import { body, param } from "express-validator";
import { isAfter, subDays, subHours, addMilliseconds } from "date-fns"; import { isAfter, subDays, subHours, addMilliseconds } from "date-fns";
import urlRegex from "url-regex"; import urlRegex from "url-regex-safe";
import { promisify } from "util"; import { promisify } from "util";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import axios from "axios"; 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 // 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 // do nothing
} }

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { CustomError } from "../utils"; import { CustomError } from "../utils";
import * as redis from "../redis"; import redisClient, * as redis from "../redis";
import knex from "../knex"; import knex from "../knex";
const selectable = [ const selectable = [
@ -96,7 +96,7 @@ export const get = async (match: Partial<Link>, params: GetParams) => {
export const find = async (match: Partial<Link>): Promise<Link> => { export const find = async (match: Partial<Link>): Promise<Link> => {
if (match.address && match.domain_id) { if (match.address && match.domain_id) {
const key = redis.key.link(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); if (cachedLink) return JSON.parse(cachedLink);
} }
@ -108,7 +108,7 @@ export const find = async (match: Partial<Link>): Promise<Link> => {
if (link) { if (link) {
const key = redis.key.link(link.address, link.domain_id); 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; 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 { addMinutes } from "date-fns";
import * as redis from "../redis"; import redisCLient, * as redis from "../redis";
import knex from "../knex"; import knex from "../knex";
export const find = async (match: Partial<User>) => { export const find = async (match: Partial<User>) => {
if (match.email || match.apikey) { if (match.email || match.apikey) {
const key = redis.key.user(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; if (cachedUser) return JSON.parse(cachedUser) as User;
} }
const user = await knex<User>("users") const user = await knex<User>("users").where(match).first();
.where(match)
.first();
if (user) { if (user) {
const emailKey = redis.key.user(user.email); 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) { if (user.apikey) {
const apikeyKey = redis.key.user(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) => { export const remove = async (user: User) => {
const deletedUser = await knex<User>("users") const deletedUser = await knex<User>("users").where("id", user.id).delete();
.where("id", user.id)
.delete();
redis.remove.user(user); redis.remove.user(user);

View File

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

View File

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

View File

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

View File

@ -1,29 +1,15 @@
import { promisify } from "util"; import Redis from "ioredis";
import redis from "redis";
import env from "./env"; import env from "./env";
const client = redis.createClient({ const client = new Redis({
host: env.REDIS_HOST, host: env.REDIS_HOST,
port: env.REDIS_PORT, port: env.REDIS_PORT,
db: env.REDIS_DB, db: env.REDIS_DB,
...(env.REDIS_PASSWORD && { password: env.REDIS_PASSWORD }) ...(env.REDIS_PASSWORD && { password: env.REDIS_PASSWORD })
}); });
export const get: (key: string) => Promise<any> = promisify(client.get).bind( export default client;
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 const key = { export const key = {
link: (address: string, domain_id?: number, user_id?: number) => link: (address: string, domain_id?: number, user_id?: number) =>
@ -37,19 +23,21 @@ export const key = {
export const remove = { export const remove = {
domain: (domain?: Domain) => { domain: (domain?: Domain) => {
if (!domain) return; if (!domain) return;
del(key.domain(domain.address)); return client.del(key.domain(domain.address));
}, },
host: (host?: Host) => { host: (host?: Host) => {
if (!host) return; if (!host) return;
del(key.host(host.address)); return client.del(key.host(host.address));
}, },
link: (link?: Link) => { link: (link?: Link) => {
if (!link) return; if (!link) return;
del(key.link(link.address, link.domain_id)); return client.del(key.link(link.address, link.domain_id));
}, },
user: (user?: User) => { user: (user?: User) => {
if (!user) return; if (!user) return;
del(key.user(user.email)); return Promise.all([
del(key.user(user.apikey)); 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(morgan("combined", { stream }));
} }
server.use(helmet()); server.use(helmet({ contentSecurityPolicy: false }));
server.use(cookieParser()); server.use(cookieParser());
server.use(express.json()); server.use(express.json());
server.use(express.urlencoded({ extended: true })); server.use(express.urlencoded({ extended: true }));
@ -68,8 +68,7 @@ app.prepare().then(async () => {
// Handler everything else by Next.js // Handler everything else by Next.js
server.get("*", (req, res) => handle(req, res)); server.get("*", (req, res) => handle(req, res));
server.listen(port, err => { server.listen(port, () => {
if (err) throw err;
console.log(`> Ready on http://localhost:${port}`); console.log(`> Ready on http://localhost:${port}`);
}); });
}); });

View File

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

View File

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